Result { get; set; }
132 | public bool IsBusy { get; set; }
133 | }
134 |
135 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_INSTRUMENTATION_KEY=
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_INSTRUMENTATION_KEY=d35b5caf-a276-467c-9ac7-f7f7d84ea171
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ServerlessLibraryAPI",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@uifabric/styling": "^6.47.6",
7 | "markdown-to-jsx": "^6.11.4",
8 | "office-ui-fabric-react": "^6.187.0",
9 | "react": "^16.8.6",
10 | "react-app-polyfill": "^1.0.1",
11 | "react-dom": "^16.8.6",
12 | "react-redux": "^6.0.1",
13 | "react-router-dom": "^4.3.1",
14 | "redux": "^4.0.1",
15 | "rimraf": "^2.6.3"
16 | },
17 | "devDependencies": {
18 | "react-scripts": "^5.0.1"
19 | },
20 | "scripts": {
21 | "start": "rimraf ./build && react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test --env=jsdom",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": [
30 | ">0.2%",
31 | "ie 11",
32 | "not dead",
33 | "not op_mini all"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure/ServerlessLibrary/22d6af9182763804124758a955b560162d393a6a/ServerlessLibraryAPI/ClientApp/public/favicon.ico
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
14 |
15 |
24 | Azure Serverless Community Library
25 |
37 |
38 |
39 |
40 | You need to enable JavaScript to run this app.
41 |
42 |
43 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "ServerlessLibraryAPI",
3 | "name": "ServerlessLibraryAPI",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/App.css:
--------------------------------------------------------------------------------
1 | /* #container { background: blue; } */
2 | #header {
3 | height: 40px;
4 | }
5 | #main {
6 | height: calc(100% - 40px);
7 | }
8 | #container {
9 | overflow-y: auto;
10 | }
11 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Switch, Route, withRouter } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { Dialog } from "office-ui-fabric-react";
5 |
6 | import "./App.css";
7 |
8 | import Main from "./components/Main/Main";
9 | import Header from "./components/Header/Header";
10 | import DetailView from "./components/DetailView/DetailView";
11 | import ContributionsPage from "./components/Contribute/Contribute";
12 | import { sampleActions } from "./actions/sampleActions";
13 | import { userActions } from "./actions/userActions";
14 | import { libraryService, userService } from "./services";
15 |
16 | const loginErrorMsg = "We were unable to log you in. Please try again later.";
17 |
18 | class App extends Component {
19 | constructor(props) {
20 | super(props);
21 |
22 | this.state = {
23 | showErrorDialog: false
24 | };
25 | this.onDismissErrorDialog = this.onDismissErrorDialog.bind(this);
26 | }
27 |
28 | componentDidMount() {
29 | libraryService
30 | .getAllSamples()
31 | .then(samples => this.props.getSamplesSuccess(samples))
32 | .catch(() => {
33 | // do nothing
34 | });
35 |
36 | this.props.getCurrentUserRequest();
37 | userService
38 | .getCurrentUser()
39 | .then(user => this.props.getCurrentUserSuccess(user))
40 | .catch(data => {
41 | this.props.getCurrentUserFailure();
42 | if (data.status !== 401) {
43 | this.setState({
44 | showErrorDialog: true
45 | });
46 | }
47 | });
48 | }
49 |
50 | onDismissErrorDialog() {
51 | this.setState({
52 | showErrorDialog: false
53 | });
54 | }
55 |
56 | render() {
57 | const { showErrorDialog } = this.state;
58 | return (
59 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {showErrorDialog && (
71 |
78 | {loginErrorMsg}
79 |
80 | )}
81 |
82 | );
83 | }
84 | }
85 |
86 | function mapStateToProps(state) {
87 | return {};
88 | }
89 |
90 | const mapDispatchToProps = {
91 | getSamplesSuccess: sampleActions.getSamplesSuccess,
92 | getCurrentUserRequest: userActions.getCurrentUserRequest,
93 | getCurrentUserSuccess: userActions.getCurrentUserSuccess,
94 | getCurrentUserFailure: userActions.getCurrentUserFailure
95 | };
96 | const AppContainer = withRouter(
97 | connect(
98 | mapStateToProps,
99 | mapDispatchToProps
100 | )(App)
101 | );
102 |
103 | export default AppContainer;
104 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const sampleActionTypes = {
2 | GETSAMPLES_SUCCESS: "GETSAMPLES_SUCCESS"
3 | };
4 |
5 | export const userActionTypes = {
6 | GETCURRENTUSER_REQUEST: "GETCURRENTUSER_REQUEST",
7 | GETCURRENTUSER_SUCCESS: "GETCURRENTUSER_SUCCESS",
8 | GETCURRENTUSER_FAILURE: "GETCURRENTUSER_FAILURE",
9 | LOGOUT: "LOGOUT"
10 | };
11 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/actions/sampleActions.js:
--------------------------------------------------------------------------------
1 | import { sampleActionTypes } from "./actionTypes";
2 |
3 | export const sampleActions = {
4 | getSamplesSuccess
5 | };
6 |
7 | function getSamplesSuccess(samples) {
8 | return {
9 | type: sampleActionTypes.GETSAMPLES_SUCCESS,
10 | samples
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import { userActionTypes } from "./actionTypes";
2 |
3 | export const userActions = {
4 | getCurrentUserRequest,
5 | getCurrentUserSuccess,
6 | getCurrentUserFailure,
7 | logout
8 | };
9 |
10 | function getCurrentUserRequest(user) {
11 | return {
12 | type: userActionTypes.GETCURRENTUSER_REQUEST,
13 | user
14 | };
15 | }
16 |
17 | function getCurrentUserSuccess(user) {
18 | return {
19 | type: userActionTypes.GETCURRENTUSER_SUCCESS,
20 | user
21 | };
22 | }
23 |
24 | function getCurrentUserFailure(user) {
25 | return {
26 | type: userActionTypes.GETCURRENTUSER_FAILURE,
27 | user
28 | };
29 | }
30 |
31 | function logout() {
32 | return {
33 | type: userActionTypes.LOGOUT
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/assets/contribution.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/assets/functionapp.svg:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
17 |
21 |
27 |
28 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/assets/logicapp.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ContentHeader/ContentHeader.css:
--------------------------------------------------------------------------------
1 | .content-header-titlewapper {
2 | display: flex;
3 | margin-top: 20px;
4 | margin-bottom: 20px;
5 | }
6 | .content-header-title {
7 | font-size: 18px;
8 | font-weight: bold;
9 | }
10 | .content-header-contributionLink {
11 | text-decoration: none;
12 | }
13 | .contributionLink-content {
14 | font-size: 12px;
15 | display: flex;
16 | text-decoration: none;
17 | }
18 |
19 | .contribution-link-text {
20 | margin-left: 12px;
21 | }
22 | .content-header-sortbywrappper {
23 | display: flex;
24 | margin-top: 20px;
25 | margin-bottom: 5px;
26 | }
27 |
28 | .content-header-count {
29 | text-align: left;
30 | margin-top: auto;
31 | margin-bottom: auto;
32 | color: #000000;
33 | font-size: 16px;
34 | line-height: normal;
35 | }
36 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ContentHeader/ContentHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { withRouter } from "react-router-dom";
4 | import {
5 | SearchBox,
6 | Dropdown,
7 | Icon,
8 | Link as FabricLink
9 | } from "office-ui-fabric-react";
10 | import { Link } from "react-router-dom";
11 | import {
12 | paramsToQueryString,
13 | queryStringToParams,
14 | trackEvent
15 | } from "../../helpers";
16 | import "./ContentHeader.css";
17 |
18 | class ContentHeader extends Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | filterText: this.props.initialSearchText,
23 | sortby: this.props.initialSortBy
24 | };
25 | }
26 |
27 | filterTextChanged(newValue) {
28 | var params = queryStringToParams(this.props.location.search);
29 | delete params["filtertext"];
30 | if (newValue && newValue !== "") {
31 | params["filtertext"] = newValue;
32 | }
33 |
34 | this.setState({ filterText: newValue });
35 | this.props.history.push(paramsToQueryString(params));
36 | trackEvent("/filter/change/searchtext", newValue);
37 | }
38 |
39 | sortbyChanged(newValue) {
40 | var params = queryStringToParams(this.props.location.search);
41 | delete params["sortby"];
42 | if (newValue !== "totaldownloads") {
43 | params["sortby"] = newValue;
44 | }
45 | this.setState({ sortby: newValue });
46 | this.props.history.push(paramsToQueryString(params));
47 | trackEvent("/sortby/change", newValue);
48 | }
49 |
50 | render() {
51 | const dropdownStyles = () => {
52 | return {
53 | root: {
54 | display: "flex"
55 | },
56 | label: {
57 | marginRight: "10px",
58 | color: "#000000",
59 | fontSize: "12px"
60 | },
61 | title: {
62 | color: "#595959;",
63 | border: "1px solid #BCBCBC",
64 | borderRadius: "2px",
65 | fontSize: "12px"
66 | },
67 | dropdown: {
68 | width: 150
69 | }
70 | };
71 | };
72 |
73 | const searchBoxStyles = () => {
74 | return {
75 | root: {
76 | border: "1px solid #BCBCBC",
77 | borderRadius: "3px"
78 | }
79 | };
80 | };
81 |
82 | let resultCount = this.props.samples.length;
83 | return (
84 |
85 |
86 |
87 | Azure serverless community library
88 |
89 |
90 |
95 |
96 |
97 |
Contributions
98 |
99 |
100 |
101 |
102 |
this.filterTextChanged(newValue)}
106 | onClear={() => this.filterTextChanged("")}
107 | styles={searchBoxStyles}
108 | />
109 |
110 |
111 | Displaying {resultCount} {resultCount === 1 ? "result" : "results"}
112 |
113 |
114 | this.sortbyChanged(item.key)}
124 | />
125 |
126 |
127 |
128 | );
129 | }
130 | }
131 |
132 | const mapStateToProps = state => ({});
133 |
134 | const ContentHeaderContainer = connect(mapStateToProps)(ContentHeader);
135 |
136 | export default withRouter(ContentHeaderContainer);
137 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/Contribute.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ContributionForm from "./ContributionForm";
3 | import ContributionsList from "./ContributionsList";
4 | import SignInDialog from "./SignInDialog";
5 | import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton";
6 |
7 | import "./Contribute.scss";
8 |
9 | class ContributionsPage extends Component {
10 | render() {
11 | return (
12 |
13 |
14 |
15 | This is where you can see all your existing contributions. You can
16 | also add a new contribution by clicking on the add new contribution
17 | link.
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default ContributionsPage;
29 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/Contribute.scss:
--------------------------------------------------------------------------------
1 | .left-margin {
2 | margin-left: 23px;
3 | }
4 |
5 | .contribute-page-description {
6 | max-width: 785px;
7 | }
8 |
9 | .add-contribution-container {
10 | margin-top: 23px;
11 | margin-bottom: 22px;
12 | }
13 |
14 | .add-contribution-link {
15 | @extend .left-margin;
16 | font-size: 12px;
17 | }
18 |
19 | .contribution-form-container {
20 | background-color: #fbfbfb;
21 | margin-top: 12px;
22 | padding-top: 15px;
23 | padding-left: 23px;
24 | padding-bottom: 28px;
25 | }
26 | .input-container {
27 | display: flex;
28 | flex-direction: row;
29 | flex: 1;
30 | }
31 | .contribution-form-fields-container {
32 | max-width: 430px;
33 | margin-right: 100px;
34 | flex: 1;
35 | }
36 | .input-field-container {
37 | margin-bottom: 10px;
38 | }
39 |
40 | .contribution-form-actions-container {
41 | margin-top: 18px;
42 | }
43 |
44 | .contribution-list-container {
45 | @extend .left-margin;
46 | max-width: 746px;
47 | }
48 |
49 | .empty-contribution-list-container {
50 | font-size: 12px;
51 | margin-top: 6px;
52 | }
53 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import {
3 | Icon,
4 | Link,
5 | TextField,
6 | PrimaryButton,
7 | DefaultButton,
8 | Dropdown,
9 | Dialog
10 | } from "office-ui-fabric-react";
11 |
12 | import { libraryService } from "../../services";
13 | import * as commonStyles from "../shared/Button.styles";
14 | import * as Constants from "../shared/Constants";
15 |
16 | const initialState = {
17 | showForm: false,
18 | title: "",
19 | description: "",
20 | repository: "",
21 | template: "",
22 | technologies: [],
23 | language: "",
24 | solutionareas: [],
25 | dialogProps: {
26 | isVisible: false,
27 | title: "",
28 | content: {}
29 | }
30 | };
31 |
32 | class ContributionForm extends Component {
33 | constructor(props) {
34 | super(props);
35 |
36 | this.state = initialState;
37 |
38 | this.onLinkClick = this.onLinkClick.bind(this);
39 | this.onAddButtonClick = this.onAddButtonClick.bind(this);
40 | this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
41 | this.handleInputChange = this.handleInputChange.bind(this);
42 | this.onTitleChange = this.onTitleChange.bind(this);
43 | this.onDescriptionChange = this.onDescriptionChange.bind(this);
44 | this.onDismissDialog = this.onDismissDialog.bind(this);
45 | this.validateForm = this.getFormValidationErrors.bind(this);
46 | }
47 |
48 | technologiesOptionsChanged(newValue) {
49 | let technologies = this.state.technologies;
50 | if (newValue.selected) {
51 | technologies.push(newValue.key);
52 | } else {
53 | technologies = technologies.filter(t => t !== newValue.key);
54 | }
55 |
56 | this.setState({ technologies: technologies });
57 | }
58 |
59 | solutionAreasOptionsChanged(newValue) {
60 | let solutionAreas = this.state.solutionareas;
61 | if (newValue.selected) {
62 | solutionAreas.push(newValue.key);
63 | } else {
64 | solutionAreas = solutionAreas.filter(t => t !== newValue.key);
65 | }
66 |
67 | this.setState({ solutionareas: solutionAreas });
68 | }
69 |
70 | languageOptionChanged(newValue) {
71 | this.setState({ language: newValue });
72 | }
73 |
74 | handleInputChange(event, newValue) {
75 | const target = event.target;
76 | const name = target.name;
77 | this.setState({ [name]: newValue });
78 | }
79 |
80 | onTitleChange(event, newValue) {
81 | if (!newValue || newValue.length <= 80) {
82 | this.setState({ title: newValue || "" });
83 | } else {
84 | // this block is needed because of Fabric bug #1350
85 | this.setState({ title: this.state.title });
86 | }
87 | }
88 |
89 | onDescriptionChange(event, newValue) {
90 | if (!newValue || newValue.length <= 300) {
91 | this.setState({ description: newValue || "" });
92 | } else {
93 | // this block is needed because of Fabric bug #1350
94 | this.setState({ description: this.state.description });
95 | }
96 | }
97 |
98 | getFormValidationErrors(sample) {
99 | let errors = [];
100 | if (sample.title.length === 0) {
101 | errors.push("Title cannot be empty");
102 | }
103 | if (sample.repository.length === 0) {
104 | errors.push("Repository URL cannot be empty");
105 | }
106 | if (sample.description.length === 0) {
107 | errors.push("Description cannot be empty");
108 | }
109 | if (sample.technologies.length === 0) {
110 | errors.push("At least one technology must be selected");
111 | }
112 | if (sample.language.length === 0) {
113 | errors.push("Language must be selected");
114 | }
115 | if (sample.solutionareas.length === 0) {
116 | errors.push("At least one solution area must be selected");
117 | }
118 |
119 | return errors;
120 | }
121 |
122 | onLinkClick() {
123 | this.setState({ showForm: true });
124 | }
125 |
126 | onAddButtonClick() {
127 | const sample = {
128 | title: this.state.title,
129 | description: this.state.description,
130 | repository: this.state.repository,
131 | template: this.state.template,
132 | technologies: this.state.technologies,
133 | language: this.state.language,
134 | solutionareas: this.state.solutionareas
135 | };
136 | const errors = this.getFormValidationErrors(sample);
137 | if (errors.length > 0) {
138 | this.showDialog("Unable to add sample", errors);
139 | return;
140 | }
141 |
142 | libraryService
143 | .submitNewSample(sample)
144 | .then(sample => {
145 | this.resetForm();
146 | this.showDialog(
147 | "Thank you!",
148 | "Thank you for your contribution! Your sample has been submitted for approval."
149 | );
150 | })
151 | .catch(data => {
152 | this.showDialog("Unable to add sample", data.error);
153 | });
154 | }
155 |
156 | showDialog(title, content) {
157 | const dialogProps = {
158 | title: title,
159 | content: content,
160 | isVisible: true
161 | };
162 | this.setState({ dialogProps });
163 | }
164 |
165 | onDismissDialog() {
166 | const dialogProps = { ...this.state.dialogProps, isVisible: false };
167 | this.setState({ dialogProps });
168 | }
169 |
170 | onCancelButtonClick() {
171 | this.resetForm();
172 | }
173 |
174 | resetForm() {
175 | this.setState(initialState);
176 | }
177 |
178 | render() {
179 | let technologiesOptions = Constants.technologies.map(t => ({
180 | key: t,
181 | text: t
182 | }));
183 | let languageOptions = Constants.languages.map(l => ({ key: l, text: l }));
184 | languageOptions.push({
185 | key: Constants.NotApplicableLanguage,
186 | text: "Not applicable"
187 | });
188 | let solutionAreasOptions = Constants.solutionAreas.map(s => ({
189 | key: s,
190 | text: s
191 | }));
192 | solutionAreasOptions.push({
193 | key: Constants.OtherSolutionArea,
194 | text: Constants.OtherSolutionArea
195 | });
196 | let { showForm, dialogProps } = this.state;
197 | return (
198 |
199 |
200 |
201 |
202 | Add new contribution
203 |
204 |
205 | {dialogProps.isVisible && (
206 |
213 | {Array.isArray(dialogProps.content) ? (
214 |
215 | {dialogProps.content.map((message, index) => (
216 | {message}
217 | ))}
218 |
219 | ) : (
220 | {String(dialogProps.content)}
221 | )}
222 |
223 | )}
224 | {showForm && (
225 |
226 |
227 |
228 |
237 |
246 |
258 |
259 |
260 | this.technologiesOptionsChanged(item)}
265 | required={true}
266 | multiSelect
267 | options={technologiesOptions}
268 | />
269 | this.languageOptionChanged(item.key)}
274 | required={true}
275 | options={languageOptions}
276 | />
277 |
282 | this.solutionAreasOptionsChanged(item)
283 | }
284 | required={true}
285 | multiSelect
286 | options={solutionAreasOptions}
287 | />
288 |
296 |
297 |
298 |
310 |
311 | )}
312 |
313 | );
314 | }
315 | }
316 |
317 | export default ContributionForm;
318 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionForm.styles.js:
--------------------------------------------------------------------------------
1 | const labelStyles = {
2 | root: {
3 | fontSize: "13px",
4 | lineHeight: "18px",
5 | height: "21px",
6 | paddingBottom: "1px",
7 | paddingTop: 0
8 | }
9 | };
10 |
11 | export const textFieldStyles = {
12 | root: {
13 | marginBottom: "10px"
14 | },
15 | fieldGroup: {
16 | height: "23px"
17 | },
18 | field: {
19 | fontSize: "12px",
20 | paddingLeft: "7px",
21 | selectors: {
22 | "::placeholder": {
23 | fontSize: "12px"
24 | }
25 | }
26 | },
27 | subComponentStyles: {
28 | label: labelStyles
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/ContributionsList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { Label } from "office-ui-fabric-react";
4 | import ItemList from "../../components/ItemList/ItemList";
5 |
6 | class ContributionsList extends Component {
7 | filteredSamples() {
8 | let { loggedIn, user, samples } = this.props;
9 | let { userName } = user;
10 | if (!loggedIn || !userName) {
11 | return {};
12 | }
13 |
14 | let filter = new RegExp(userName, "i");
15 | samples = samples.filter(
16 | el =>
17 | el.repository.replace("https://github.com/", "").match(filter) || // this match should be against author
18 | (el.author && el.author.match(filter))
19 | );
20 |
21 | return samples;
22 | }
23 |
24 | render() {
25 | const headerLabelStyles = {
26 | root: {
27 | fontSize: "12px",
28 | fontWeight: "bold",
29 | paddingTop: "0px",
30 | paddingBottom: "6px"
31 | }
32 | };
33 | const filteredSamples = this.filteredSamples();
34 | return (
35 |
36 |
My samples
37 | {filteredSamples.length > 0 ? (
38 |
39 | ) : (
40 |
41 | You currently do not have any samples
42 |
43 | )}
44 |
45 | );
46 | }
47 | }
48 |
49 | function mapStateToProps(state) {
50 | return {
51 | samples: state.samples,
52 | loggedIn: state.authentication.loggedIn,
53 | user: state.authentication.user
54 | };
55 | }
56 |
57 | const ContributionsListContainer = connect(mapStateToProps)(ContributionsList);
58 |
59 | export default ContributionsListContainer;
60 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Contribute/SignInDialog.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { Dialog, DialogFooter, DefaultButton } from "office-ui-fabric-react";
5 |
6 | import SignInButton from "../shared/SignInButton";
7 |
8 | class SignInDialog extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this);
12 | }
13 |
14 | handleHomeButtonClick() {
15 | this.props.history.push("/");
16 | }
17 |
18 | render() {
19 | const footerStyles = {
20 | actionsRight: {
21 | textAlign: "center",
22 | marginRight: "0px"
23 | }
24 | };
25 | const buttonStyles = {
26 | root: {
27 | fontSize: "12px",
28 | height: "32px",
29 | minWidth: "40px",
30 | backgroundColor: "white",
31 | border: "1px solid #0078D7",
32 | color: "#0058AD"
33 | },
34 | rootHovered: {
35 | border: "1px solid #0078D7",
36 | color: "#0058AD"
37 | },
38 | label: {
39 | fontWeight: "normal"
40 | }
41 | };
42 |
43 | const { loading, loggedIn } = this.props;
44 | if (loading) {
45 | return null;
46 | }
47 |
48 | return (
49 |
50 |
60 |
61 |
62 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | const mapStateToProps = state => ({
75 | loading: state.authentication.loading,
76 | loggedIn: state.authentication.loggedIn
77 | });
78 |
79 | const SignInDialogContainer = connect(mapStateToProps)(SignInDialog);
80 |
81 | export default withRouter(SignInDialogContainer);
82 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/ActionBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Icon, Link as FabricLink } from "office-ui-fabric-react";
3 | import { trackEvent } from "../../helpers";
4 | import { libraryService } from "../../services";
5 | import "./ActionBar.scss";
6 |
7 | class ActionBar extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.outboundRepoClick = this.outboundRepoClick.bind(this);
11 | this.outboundDeployClick = this.outboundDeployClick.bind(this);
12 | this.openInVSCodeClick = this.openInVSCodeClick.bind(this);
13 | this.trackUserActionEvent = this.trackUserActionEvent.bind(this);
14 | }
15 |
16 | outboundDeployClick() {
17 | this.updateDownloadCount(this.props.id);
18 | this.trackUserActionEvent("/sample/deploy/agree");
19 | }
20 |
21 | updateDownloadCount(id) {
22 | libraryService
23 | .updateDownloadCount(id)
24 | .then(() => {
25 | // do nothing
26 | })
27 | .catch(() => {
28 | // do nothing
29 | });
30 | }
31 |
32 | getDeployLink(template) {
33 | return (
34 | "https://portal.azure.com/#create/Microsoft.Template/uri/" +
35 | encodeURIComponent(template)
36 | );
37 | }
38 |
39 | outboundRepoClick() {
40 | this.trackUserActionEvent("/sample/source");
41 | }
42 |
43 | getOpenInVSCodeLink(repository) {
44 | return "vscode://vscode.git/clone?url=" + encodeURIComponent(repository);
45 | }
46 |
47 | openInVSCodeClick() {
48 | this.updateDownloadCount(this.props.id);
49 | this.trackUserActionEvent("/sample/openinvscode");
50 | }
51 |
52 | trackUserActionEvent(eventName) {
53 | let eventData = {
54 | id: this.props.id,
55 | repository: this.props.repository,
56 | template: this.props.template
57 | };
58 |
59 | trackEvent(eventName, eventData);
60 | }
61 |
62 | render() {
63 | const { repository, template } = this.props;
64 |
65 | return (
66 |
67 |
68 |
73 |
74 |
75 | Edit in VS Code
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 | Deploy
89 |
90 |
91 |
92 |
93 |
99 |
100 |
101 | Open in Github
102 |
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
110 | export default ActionBar;
111 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/ActionBar.scss:
--------------------------------------------------------------------------------
1 | .action-container {
2 | display: flex;
3 | flex-direction: row;
4 | padding-top: 7px;
5 | padding-left: 23px;
6 | }
7 |
8 | .action-item {
9 | margin-right: 16px;
10 | }
11 |
12 | .action-link-wrapper {
13 | height: 100%;
14 | display: flex;
15 | }
16 |
17 | .fabric-icon-link {
18 | font-size: 12px;
19 | width: 13px;
20 | height: 11px;
21 | margin-top: 2px;
22 | }
23 |
24 | .githubicon {
25 | @extend .fabric-icon-link;
26 | fill: currentColor;
27 | }
28 |
29 | .action-link-text {
30 | padding-left: 8px;
31 | font-size: 12px;
32 | margin: auto;
33 | }
34 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageContent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactMarkdown from "markdown-to-jsx";
3 | import {
4 | Pivot,
5 | PivotItem,
6 | PivotLinkSize,
7 | ScrollablePane
8 | } from "office-ui-fabric-react/lib/index";
9 | import { githubService } from "../../services";
10 | import "./DetailPageContent.scss";
11 |
12 | const defaultLicenseText =
13 | "Each application is licensed to you by its owner (which may or may not be Microsoft) under the agreement which accompanies the application. Microsoft is not responsible for any non-Microsoft code and does not screen for security, compatibility, or performance. The applications are not supported by any Microsoft support program or service. The applications are provided AS IS without warranty of any kind.";
14 |
15 | class DetailPageContent extends Component {
16 | constructor(props) {
17 | super(props);
18 | this.state = {
19 | armTemplateText: "",
20 | markdownText: "",
21 | licenseText: "",
22 | selectedKey: "overview"
23 | };
24 |
25 | this.handleLinkClick = this.handleLinkClick.bind(this);
26 | }
27 |
28 | // This method is used to fetch readme content from repo when valid repository url is received as props
29 | componentDidUpdate(prevProps, prevState) {
30 | if (
31 | this.props.repository !== prevProps.repository &&
32 | prevState.markdownText === ""
33 | ) {
34 | let { repository } = this.props;
35 | githubService
36 | .getReadMe(repository)
37 | .then(data => {
38 | var r = new RegExp(
39 | "https?://Azuredeploy.net/deploybutton.(png|svg)",
40 | "ig"
41 | );
42 | data = data.replace(r, "");
43 | this.setState({ markdownText: data });
44 | })
45 | .catch(() => {
46 | // do nothing
47 | });
48 | }
49 | }
50 |
51 | handleLinkClick(pivotItem, ev) {
52 | const selectedKey = pivotItem.props.itemKey;
53 | this.setState({ selectedKey });
54 | if (selectedKey === "armtemplate" && this.state.armTemplateText === "") {
55 | const { template } = this.props;
56 | if (template) {
57 | githubService
58 | .getArmTemplate(template)
59 | .then(data =>
60 | this.setState({
61 | armTemplateText: data
62 | })
63 | )
64 | .catch(() => {
65 | // do nothing
66 | });
67 | }
68 | }
69 | if (selectedKey === "license" && this.state.licenseText === "") {
70 | const { license, repository } = this.props;
71 | githubService
72 | .getLicense(license, repository)
73 | .then(data =>
74 | this.setState({
75 | licenseText: data
76 | })
77 | )
78 | .catch(() => {
79 | // do nothing
80 | });
81 | }
82 | }
83 |
84 | render() {
85 | const pivotStyles = {
86 | root: {
87 | paddingLeft: "15px"
88 | },
89 | text: {
90 | color: " #0058AD",
91 | fontSize: "14px"
92 | }
93 | };
94 |
95 | const {
96 | selectedKey,
97 | markdownText,
98 | licenseText,
99 | armTemplateText
100 | } = this.state;
101 | return (
102 |
103 |
this.handleLinkClick(item, ev)}
108 | >
109 |
110 |
111 |
112 |
113 | {markdownText}
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
{defaultLicenseText}
124 | {licenseText !== "" && (
125 |
{licenseText}
126 | )}
127 |
128 |
129 |
130 |
131 |
132 | {this.props.template && (
133 |
134 |
135 |
136 |
137 |
138 |
{armTemplateText}
139 |
140 |
141 |
142 |
143 |
144 | )}
145 |
146 |
147 | );
148 | }
149 | }
150 |
151 | export default DetailPageContent;
152 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageContent.scss:
--------------------------------------------------------------------------------
1 | .detail-page-content {
2 | margin-top: 24px;
3 | border-top: 1px solid rgba(105, 130, 155, 0.25);
4 | border-bottom: 1px solid rgba(105, 130, 155, 0.25);
5 | }
6 | .pivot-item-container {
7 | padding-left: 23px;
8 | background-color: #fbfbfb;
9 | }
10 | .scrollablePane-wrapper {
11 | min-height: 60vh;
12 | position: relative;
13 | max-height: inherit;
14 | }
15 |
16 | .armtemplate-content {
17 | overflow-wrap: break-word;
18 | white-space: pre-line;
19 | justify-content: center;
20 | }
21 |
22 | .license-content {
23 | max-width: 750px;
24 | text-align: justify;
25 | white-space: pre-line;
26 | }
27 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailPageHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PageHeaderWithBackButton from "../shared/PageHeaderWithBackButton";
3 | import MetricBar from "../MetricBar/MetricBar";
4 |
5 | class DetailPageHeader extends Component {
6 | render() {
7 | let {
8 | title,
9 | author,
10 | id,
11 | totaldownloads,
12 | createddate,
13 | description,
14 | likes,
15 | dislikes
16 | } = this.props;
17 | return (
18 |
19 |
20 |
28 | {description}
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default DetailPageHeader;
36 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/DetailView/DetailView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import ActionBar from "./ActionBar";
4 | import DetailPageContent from "./DetailPageContent";
5 | import DetailPageHeader from "./DetailPageHeader";
6 |
7 | import { sampleActions } from "../../actions/sampleActions";
8 | import { trackEvent } from "../../helpers";
9 |
10 | class DetailView extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | sample: {}
15 | };
16 | }
17 |
18 | componentDidMount() {
19 | this.setCurrentItemInState();
20 | }
21 |
22 | componentDidUpdate(prevProps, prevState) {
23 | this.setCurrentItemInState();
24 | }
25 |
26 | setCurrentItemInState() {
27 | if (!this.state.sample.id && this.props.samples.length > 0) {
28 | const id = this.props.match.params.id;
29 | let currentItem = this.props.samples.filter(s => s.id === id)[0] || {};
30 | this.setState({ sample: currentItem });
31 | this.trackPageLoadEvent(currentItem);
32 | }
33 | }
34 |
35 | trackPageLoadEvent(sample) {
36 | let eventData = {
37 | id: sample.id,
38 | repository: sample.repository,
39 | template: sample.template
40 | };
41 | trackEvent("/sample/detailpage", eventData);
42 | }
43 |
44 | render() {
45 | let likes = this.state.sample.likes ? this.state.sample.likes : 0;
46 | let dislikes = this.state.sample.dislikes ? this.state.sample.dislikes : 0;
47 | return (
48 |
69 | );
70 | }
71 | }
72 |
73 | const mapStateToProps = state => ({
74 | samples: state.samples
75 | });
76 |
77 | const mapDispatchToProps = {
78 | getSamplesSuccess: sampleActions.getSamplesSuccess
79 | };
80 |
81 | const DetailViewContainer = connect(
82 | mapStateToProps,
83 | mapDispatchToProps
84 | )(DetailView);
85 |
86 | export default DetailViewContainer;
87 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Header/AuthControl.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { ActionButton, ContextualMenuItemType } from "office-ui-fabric-react";
5 |
6 | import { userService } from "../../services";
7 | import { userActions } from "../../actions/userActions";
8 | import SignInButton from "../shared/SignInButton";
9 | import UserPersona from "./UserPersona";
10 |
11 | class AuthControl extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this._renderMenuList = this._renderMenuList.bind(this);
16 | this._onSignoutClick = this._onSignoutClick.bind(this);
17 | this._onContributionsClick = this._onContributionsClick.bind(this);
18 | }
19 |
20 | getMenuItems() {
21 | const { user } = this.props;
22 | const userName =
23 | user.fullName && user.fullName !== "" ? user.fullName : user.displayName;
24 |
25 | return [
26 | {
27 | key: "Header",
28 | itemType: ContextualMenuItemType.Header,
29 | text: "Logged in as: " + userName
30 | },
31 | {
32 | key: "divider_1",
33 | itemType: ContextualMenuItemType.Divider
34 | },
35 | {
36 | key: "contributions",
37 | text: "My contributions",
38 | onClick: this._onContributionsClick
39 | },
40 | {
41 | key: "divider_1",
42 | itemType: ContextualMenuItemType.Divider
43 | },
44 | {
45 | key: "signOut",
46 | text: "Sign out",
47 | onClick: this._onSignoutClick
48 | }
49 | ];
50 | }
51 |
52 | _renderMenuIcon() {
53 | return null;
54 | }
55 |
56 | _renderMenuList(menuListProps, defaultRender) {
57 | const { loggedIn } = this.props;
58 | if (loggedIn) {
59 | return {defaultRender(menuListProps)}
;
60 | }
61 |
62 | return (
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | _onContributionsClick() {
70 | this.props.history.push("/contribute");
71 | }
72 |
73 | _onSignoutClick() {
74 | this.props.logout(); // clear the redux store before making a call to the backend
75 | userService
76 | .logout()
77 | .then(() => {
78 | // do nothing
79 | })
80 | .catch(() => {
81 | // do nothing
82 | });
83 | }
84 |
85 | render() {
86 | return (
87 |
88 |
100 |
101 | );
102 | }
103 | }
104 |
105 | const mapStateToProps = state => ({
106 | loggedIn: state.authentication.loggedIn,
107 | user: state.authentication.user
108 | });
109 |
110 | const mapDispatchToProps = {
111 | logout: userActions.logout
112 | };
113 |
114 | const AuthControlContainer = connect(
115 | mapStateToProps,
116 | mapDispatchToProps
117 | )(AuthControl);
118 |
119 | export default withRouter(AuthControlContainer);
120 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { Link } from "office-ui-fabric-react";
4 | import { getTheme } from "office-ui-fabric-react";
5 |
6 | import "./Header.scss";
7 | import AuthControl from "./AuthControl";
8 |
9 | class Header extends Component {
10 | render() {
11 | const theme = getTheme();
12 | const linkStyles = {
13 | root: {
14 | marginLeft: "15px",
15 | lineHeight: "40px",
16 | fontSize: "14px",
17 | color: theme.palette.white,
18 | selectors: {
19 | "&:active, &:hover, &:active:hover, &:visited": {
20 | color: theme.palette.white
21 | }
22 | }
23 | }
24 | };
25 |
26 | return (
27 |
28 |
29 |
34 | Microsoft Azure
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | const mapStateToProps = state => ({
44 | loggedIn: state.authentication.loggedIn
45 | });
46 |
47 | const HeaderContainer = connect(mapStateToProps)(Header);
48 |
49 | export default HeaderContainer;
50 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Header/Header.scss:
--------------------------------------------------------------------------------
1 | .headerbar {
2 | background-color: black;
3 | display: inline-block;
4 | width: 100%;
5 | }
6 |
7 | .auth-control {
8 | float: right;
9 | }
10 |
11 | .signin-button-container {
12 | text-align: center;
13 | padding: 20px 16px;
14 | }
15 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Header/UserPersona.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { Persona, PersonaSize, Icon } from "office-ui-fabric-react";
4 | import { getTheme } from "office-ui-fabric-react";
5 |
6 | class UserPersona extends Component {
7 | _onRenderInitials() {
8 | return ;
9 | }
10 |
11 | render() {
12 | const theme = getTheme();
13 | const personaStyles = {
14 | root: {
15 | height: "40px",
16 | color: theme.palette.white,
17 | float: "right",
18 | selectors: {
19 | ":hover": {
20 | selectors: {
21 | $primaryText: {
22 | color: theme.palette.white
23 | }
24 | }
25 | }
26 | }
27 | },
28 | details: {
29 | width: "85px"
30 | },
31 | primaryText: {
32 | color: theme.palette.white
33 | }
34 | };
35 |
36 | const { loading, loggedIn, user } = this.props;
37 | if (loading) {
38 | return null;
39 | }
40 |
41 | return loggedIn ? (
42 |
53 | ) : (
54 |
61 | );
62 | }
63 | }
64 |
65 | const mapStateToProps = state => ({
66 | loading: state.authentication.loading,
67 | loggedIn: state.authentication.loggedIn,
68 | user: state.authentication.user
69 | });
70 |
71 | const UserPersonaContainer = connect(mapStateToProps)(UserPersona);
72 |
73 | export default UserPersonaContainer;
74 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ItemList/ItemList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import {
3 | FocusZone,
4 | FocusZoneDirection,
5 | List,
6 | Link as FabricLink
7 | } from "office-ui-fabric-react";
8 | import { Link } from "react-router-dom";
9 | import ItemTags from "../ItemTags/ItemTags";
10 | import MetricBar from "../MetricBar/MetricBar";
11 |
12 | import "./ItemList.scss";
13 |
14 | class ItemList extends Component {
15 | constructor(props) {
16 | super(props);
17 | this._onRenderCell = this._onRenderCell.bind(this);
18 | this.disableHover = this.props.disableHover || false;
19 | }
20 |
21 | _onRenderCell(item, index) {
22 | let likes = item.likes ? item.likes : 0;
23 | let dislikes = item.dislikes ? item.dislikes : 0;
24 | return (
25 |
26 |
27 |
32 |
33 |
38 | {item.title}
39 |
40 |
41 |
49 |
{item.description}
50 |
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | render() {
63 | return (
64 |
65 |
66 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | export default ItemList;
77 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ItemList/ItemList.scss:
--------------------------------------------------------------------------------
1 | .libraryitem {
2 | border-bottom: 1px solid #d8d8d8;
3 | padding-top: 12px;
4 | padding-bottom: 15px;
5 | }
6 |
7 | .libraryitem-withhover {
8 | @extend .libraryitem;
9 | &:hover {
10 | border-bottom: none;
11 | }
12 | }
13 |
14 | .libraryitemContainer {
15 | padding-left: 16px;
16 | &:hover {
17 | margin-top: -1px;
18 | margin-bottom: 1px;
19 | background-color: white;
20 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
21 | }
22 | }
23 |
24 | .title {
25 | font-size: 16px;
26 | line-height: 21px;
27 | color: #0072c6;
28 | &:visited {
29 | text-decoration: none;
30 | }
31 | }
32 |
33 | .titlelink {
34 | text-decoration: none;
35 | }
36 |
37 | .description {
38 | font-size: 14px;
39 | line-height: 19px;
40 | color: #000000;
41 | padding-bottom: 4px;
42 | padding-bottom: 10px;
43 | white-space: nowrap;
44 | overflow: hidden;
45 | text-overflow: ellipsis;
46 | }
47 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ItemTags/ItemTags.css:
--------------------------------------------------------------------------------
1 | .tagcontainer {
2 | font-size: 14px;
3 | line-height: normal;
4 | vertical-align: sub;
5 | }
6 |
7 | .tag {
8 | margin-left: 5px;
9 | background: #efefef;
10 | border-radius: 10px;
11 | padding: 4px 10px 4px 10px;
12 | font-size: 12px;
13 | line-height: 16px;
14 | display: inline-block;
15 | }
16 |
17 | .svg {
18 | width: 17px;
19 | height: 17px;
20 | vertical-align: baseline;
21 | margin-right: 4px;
22 | }
23 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/ItemTags/ItemTags.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import * as Constants from "../shared/Constants";
3 | import "./ItemTags.css";
4 |
5 | class ItemTags extends Component {
6 | render() {
7 | // Tags contain technologies, language, solutionareas and custom tags
8 | let allTags = [];
9 |
10 | if (this.props.technologies) {
11 | allTags.push(...this.props.technologies);
12 | }
13 | if (
14 | this.props.language !== "" &&
15 | this.props.language !== Constants.NotApplicableLanguage
16 | ) {
17 | allTags.push(this.props.language);
18 | }
19 | if (this.props.solutionareas) {
20 | let solutionareas = this.props.solutionareas.filter(
21 | s => s !== Constants.OtherSolutionArea
22 | );
23 | allTags.push(...solutionareas);
24 | }
25 |
26 | if (this.props.tags) {
27 | allTags.push(...this.props.tags);
28 | }
29 |
30 | return (
31 |
32 | Tags :
33 | {allTags.map((value, index) => (
34 |
35 | {value}
36 |
37 | ))}
38 |
39 | );
40 | }
41 | }
42 |
43 | export default ItemTags;
44 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Main/Main.css:
--------------------------------------------------------------------------------
1 | #mainContainer {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | }
6 | #sidebar {
7 | overflow-y: auto;
8 | height: 100%;
9 | max-width: 15%;
10 | min-width: 175px;
11 | background-color: #f9f9f9;
12 | box-shadow: 0px 0px 1.8px rgba(0, 0, 0, 0.12),
13 | 0px 3.2px 3.6px rgba(0, 0, 0, 0.08);
14 | }
15 |
16 | #content {
17 | display: flex;
18 | flex-direction: column;
19 | overflow: hidden;
20 | flex: 1;
21 | }
22 |
23 | #contentheader{
24 | max-width: 830px;
25 | padding-left: 40px;
26 | padding-right: 40px;
27 | }
28 | #list {
29 | flex:1;
30 | overflow-y: auto;
31 | padding: 10px;
32 | padding-left: 25px;
33 | }
34 |
35 | .list-container {
36 | max-width: 765px;
37 | }
38 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/Main/Main.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import SideBarContainer from "../../components/sidebar/SideBar";
5 | import ContentHeaderContainer from "../ContentHeader/ContentHeader";
6 | import ItemList from "../../components/ItemList/ItemList";
7 | import "./Main.css";
8 | import { queryStringToParams } from "../../helpers";
9 |
10 | class Main extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | initialFilters: this.getFiltersFromQueryParams(),
15 | showContentHeaderShadow: false
16 | };
17 |
18 | this.onListScrollEvent = this.onListScrollEvent.bind(this);
19 | }
20 |
21 | filteredSamples() {
22 | let samples = this.props.samples;
23 | let currentfilters = this.getFiltersFromQueryParams();
24 | let filter = new RegExp(currentfilters.filtertext, "i");
25 | samples = samples.filter(
26 | el =>
27 | el.title.match(filter) ||
28 | el.description.match(filter) ||
29 | el.language.match(filter) ||
30 | el.repository.replace("https://github.com/", "").match(filter) ||
31 | (el.tags && el.tags.some(x => x.match(filter))) ||
32 | (el.technologies && el.technologies.some(x => x.match(filter))) ||
33 | (el.solutionareas && el.solutionareas.some(x => x.match(filter))) ||
34 | (el.author && el.author.match(filter))
35 | );
36 |
37 | if (currentfilters.categories.technologies.length > 0) {
38 | samples = samples.filter(s =>
39 | s.technologies.some(t =>
40 | currentfilters.categories.technologies.includes(t)
41 | )
42 | );
43 | }
44 |
45 | if (currentfilters.categories.languages.length > 0) {
46 | samples = samples.filter(s =>
47 | currentfilters.categories.languages.includes(s.language)
48 | );
49 | }
50 |
51 | if (currentfilters.categories.solutionareas.length > 0) {
52 | samples = samples.filter(s =>
53 | s.solutionareas.some(a =>
54 | currentfilters.categories.solutionareas.includes(a)
55 | )
56 | );
57 | }
58 |
59 | return this.Sort(samples, currentfilters.sortby);
60 | }
61 |
62 | Sort(list, sortby) {
63 | list = list.map(a => a);
64 | if (sortby === "totaldownloads") {
65 | list = list.sort(function(a, b) {
66 | return b.totaldownloads - a.totaldownloads;
67 | });
68 | } else if (sortby === "createddate") {
69 | list.sort(function(a, b) {
70 | let dateA = new Date(a.createddate),
71 | dateB = new Date(b.createddate);
72 | return dateB - dateA;
73 | });
74 | } else {
75 | list = list.sort(function(a, b) {
76 | let titleA = a.title.toLowerCase(),
77 | titleB = b.title.toLowerCase();
78 | if (titleA < titleB)
79 | //sort string ascending
80 | return -1;
81 | if (titleA > titleB) return 1;
82 | return 0; //default return value (no sorting)
83 | });
84 | }
85 |
86 | return list;
87 | }
88 |
89 | getFiltersFromQueryParams() {
90 | var filter = {
91 | categories: {
92 | technologies: [],
93 | languages: [],
94 | solutionareas: []
95 | },
96 | filtertext: "",
97 | sortby: "totaldownloads"
98 | };
99 |
100 | var params = queryStringToParams(this.props.location.search);
101 | if (params.technology && params.technology.length > 0) {
102 | filter.categories.technologies = params.technology.split(",");
103 | }
104 |
105 | if (params.language && params.language.length > 0) {
106 | filter.categories.languages = params.language.split(",");
107 | }
108 |
109 | if (params.solutionarea && params.solutionarea.length > 0) {
110 | filter.categories.solutionareas = params.solutionarea.split(",");
111 | }
112 | if (params.filtertext && params.filtertext.length > 0) {
113 | filter.filtertext = params.filtertext;
114 | }
115 | if (params.sortby && params.sortby.length > 0) {
116 | filter.sortby = params.sortby;
117 | }
118 |
119 | return filter;
120 | }
121 |
122 | onListScrollEvent(e) {
123 | let element = e.target;
124 | if (element.scrollTop > 0) {
125 | !this.state.showContentHeaderShadow &&
126 | this.setState({ showContentHeaderShadow: true });
127 | } else {
128 | this.state.showContentHeaderShadow &&
129 | this.setState({ showContentHeaderShadow: false });
130 | }
131 | }
132 | render() {
133 | const contentheaderShadowStyle = {
134 | boxShadow: this.state.showContentHeaderShadow
135 | ? "0 4px 6px -6px #222"
136 | : "none"
137 | };
138 |
139 | return (
140 |
161 | );
162 | }
163 | }
164 |
165 | const mapStateToProps = state => ({
166 | samples: state.samples
167 | });
168 |
169 | const MainContainer = connect(mapStateToProps)(withRouter(Main));
170 |
171 | export default MainContainer;
172 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/MetricBar/MetricBar.css:
--------------------------------------------------------------------------------
1 | .metrics {
2 | margin-top: 7px;
3 | margin-bottom: 7px;
4 | font-size: 12px;
5 | line-height: 16px;
6 | color: #555555;
7 | display: flex;
8 | }
9 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/MetricBar/MetricBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { IconButton } from "office-ui-fabric-react";
3 | import { libraryService } from "../../services";
4 |
5 | import "./MetricBar.css";
6 | class MetricBar extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | sentimentAction: "none"
11 | };
12 | }
13 |
14 | static getDerivedStateFromProps(nextProps, prevState) {
15 | if (nextProps.id && nextProps.id !== prevState.id) {
16 | return { sentimentAction: localStorage.getItem(nextProps.id) };
17 | }
18 |
19 | return null;
20 | }
21 |
22 | handleLikeClick() {
23 | // If user already liked, then decrement like count and set the sentiment state to none.
24 | // If in past disliked and choose to like the sample, then decrement dislike count and increment like count
25 | // If not action taken ealier, just increment like count and set sentiment state to liked.
26 | if (this.state.sentimentAction === "liked") {
27 | this.updateSentiment("none", -1, 0);
28 | } else if (this.state.sentimentAction === "disliked") {
29 | this.updateSentiment("liked", 1, -1);
30 | } else {
31 | this.updateSentiment("liked", 1, 0);
32 | }
33 | }
34 |
35 | handleDislikeClick() {
36 | if (this.state.sentimentAction === "liked") {
37 | this.updateSentiment("disliked", -1, 1);
38 | } else if (this.state.sentimentAction === "disliked") {
39 | this.updateSentiment("none", 0, -1);
40 | } else {
41 | this.updateSentiment("disliked", 0, 1);
42 | }
43 | }
44 |
45 | updateSentiment(choice, likeChanges, dislikeChanges) {
46 | localStorage.setItem(this.props.id, choice);
47 | this.setState({ sentimentAction: choice });
48 |
49 | var sentimentPayload = {
50 | Id: this.props.id,
51 | LikeChanges: likeChanges,
52 | DislikeChanges: dislikeChanges
53 | };
54 |
55 | libraryService
56 | .updateUserSentimentStats(sentimentPayload)
57 | .then(() => {
58 | // do nothing
59 | })
60 | .catch(() => {
61 | // do nothing
62 | });
63 | }
64 |
65 | render() {
66 | let { author, downloads, createddate, likes, dislikes } = this.props;
67 | let createdonDate = new Date(createddate);
68 | let createdonLocaleDate = createdonDate.toLocaleDateString();
69 |
70 | let likeIconName = "Like";
71 | let likeTitle = "Like";
72 | let dislikeIconName = "Dislike";
73 | let dislikeTitle = "Dislike";
74 |
75 | if (this.state.sentimentAction === "liked") {
76 | likeIconName = "LikeSolid";
77 | likeTitle = "Liked";
78 | likes = likes + 1;
79 | }
80 |
81 | if (this.state.sentimentAction === "disliked") {
82 | dislikeIconName = "DislikeSolid";
83 | dislikeTitle = "Disliked";
84 | dislikes = dislikes + 1;
85 | }
86 |
87 | const styles = {
88 | button: {
89 | width: 16,
90 | height: 16,
91 | padding: 0,
92 | marginLeft: 7,
93 | marginRight: 7
94 | }
95 | };
96 | return (
97 |
98 |
99 |
100 | By: {author} | {downloads}{" "}
101 | {downloads === 1 ? "download" : "downloads"} | Created on:{" "}
102 | {createdonLocaleDate} |
103 |
104 |
105 |
106 |
this.handleLikeClick()}
114 | />
115 | {likes}
116 |
117 | this.handleDislikeClick()}
125 | />
126 | {dislikes}
127 |
128 | );
129 | }
130 | }
131 |
132 | export default MetricBar;
133 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/shared/Button.styles.js:
--------------------------------------------------------------------------------
1 | import { mergeStyleSets } from "office-ui-fabric-react";
2 |
3 | export const buttonStyles = {
4 | root: {
5 | fontSize: "12px",
6 | height: "25px",
7 | marginRight: "8px",
8 | minWidth: "0px",
9 | paddingRight: "10px",
10 | paddingLeft: "10px"
11 | },
12 | label: {
13 | fontWeight: "normal"
14 | }
15 | };
16 |
17 | const secondaryButtonAdditionalStyles = {
18 | root: {
19 | backgroundColor: "white",
20 | border: "1px solid #0078D7",
21 | color: "#0058AD"
22 | },
23 | rootHovered: {
24 | // backgroundColor: "white",
25 | border: "1px solid #0078D7",
26 | color: "#0058AD"
27 | }
28 | };
29 |
30 | export const secondaryButtonStyles = mergeStyleSets(
31 | buttonStyles,
32 | secondaryButtonAdditionalStyles
33 | );
34 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/shared/Constants.js:
--------------------------------------------------------------------------------
1 | export const technologies = [
2 | "Functions 3.x",
3 | "Functions 2.x",
4 | "Functions 1.x",
5 | "Logic Apps",
6 | "Blob Storage",
7 | "Storage Queue",
8 | "Cosmos DB",
9 | "Cognitive Services",
10 | "Azure Active Directory",
11 | "App Service",
12 | "Key Vault",
13 | "SQL Server",
14 | "Service Bus Queue",
15 | "Event Grid"
16 | ];
17 |
18 | export const solutionAreas = [
19 | "Web API",
20 | "Data Processing",
21 | "Integration",
22 | "Authentication",
23 | "Automation",
24 | "Event Processing",
25 | "Machine Learning",
26 | "Scheduled Jobs",
27 | "Static Website",
28 | "Gaming",
29 | "IoT"
30 | ];
31 |
32 | export const languages = [
33 | "JavaScript",
34 | "TypeScript",
35 | "Java",
36 | "C#",
37 | "C# Script",
38 | "F#",
39 | "Python",
40 | "PowerShell"
41 | ];
42 |
43 | export const NotApplicableLanguage = "na";
44 | export const OtherSolutionArea = "Other";
45 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/shared/PageHeaderWithBackButton.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { IconButton } from "office-ui-fabric-react";
4 |
5 | import "./PageHeaderWithBackButton.scss";
6 |
7 | class PageHeaderWithBackButton extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.handleHomeButtonClick = this.handleHomeButtonClick.bind(this);
11 | }
12 |
13 | handleHomeButtonClick() {
14 | this.props.history.push("/");
15 | }
16 |
17 | render() {
18 | let { title } = this.props;
19 |
20 | const homeButton = {
21 | button: {
22 | width: 17,
23 | height: 18,
24 | marginRight: 12
25 | }
26 | };
27 |
28 | return (
29 |
30 |
31 |
32 | this.handleHomeButtonClick()}
38 | />
39 |
40 |
41 | {title}
42 |
43 |
44 |
{this.props.children}
45 |
46 | );
47 | }
48 | }
49 |
50 | export default withRouter(PageHeaderWithBackButton);
51 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/shared/PageHeaderWithBackButton.scss:
--------------------------------------------------------------------------------
1 | .page-header {
2 | padding-top: 20px;
3 | // margin-bottom: 23px; // this cannot be set at the common header control because of pivot control sizes
4 | background: #ffffff;
5 | padding-left: 23px;
6 | }
7 | .page-title-container {
8 | display: flex;
9 | flex-direction: row;
10 | margin-bottom: 11px;
11 | }
12 |
13 | .back-button-icon-container {
14 | margin: auto 0;
15 | height: 18px;
16 | }
17 |
18 | .page-title {
19 | font-size: 18px;
20 | font-weight: bold;
21 | color: black;
22 | margin-bottom: auto;
23 | }
24 |
25 | .page-description-container {
26 | font-size: 12px;
27 | line-height: 14px;
28 | color: #161616;
29 | }
30 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/shared/SignInButton.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { PrimaryButton } from "office-ui-fabric-react";
3 |
4 | class SignInButton extends Component {
5 | handleButtonClick() {
6 | const currentLocation = encodeURIComponent(window.location);
7 | window.location = `/api/user/login?returnUrl=${currentLocation}`;
8 | }
9 |
10 | render() {
11 | const buttonStyles = {
12 | root: {
13 | fontSize: "12px",
14 | height: "32px"
15 | },
16 | label: {
17 | fontWeight: "normal"
18 | }
19 | };
20 |
21 | return (
22 |
28 | Sign in with GitHub
29 |
30 | );
31 | }
32 | }
33 |
34 | export default SignInButton;
35 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/sidebar/SideBar.css:
--------------------------------------------------------------------------------
1 | .sidebar-wrapper{
2 | padding-left: 24px;
3 | padding-top: 20px;
4 | padding-right: 20px;
5 | }
6 | .filterby-title {
7 | font-size: 18px;
8 | line-height: 24px;
9 | margin-bottom: 12px;
10 | }
11 |
12 | .Filter-list-header{
13 | font-size: 14px;
14 | line-height: 16px;
15 | color: #555555;
16 | display: inline-block;
17 | }
18 |
19 | .filterset{
20 | margin-bottom: 23px;
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/components/sidebar/SideBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { withRouter } from "react-router-dom";
4 | import { Checkbox } from "office-ui-fabric-react/lib/index";
5 | import {
6 | paramsToQueryString,
7 | queryStringToParams,
8 | trackEvent
9 | } from "../../helpers";
10 |
11 | import * as Constants from "../shared/Constants";
12 | import "./SideBar.css";
13 |
14 | class SideBar extends Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | filters: this.props.initialFilters
19 | };
20 | }
21 |
22 | isChecked(category, item) {
23 | return this.state.filters[category].includes(item);
24 | }
25 |
26 | checkboxclicked(ev, checked, category, item) {
27 | var currentFilters = this.state.filters;
28 | var categoryArray = currentFilters[category];
29 | if (!checked) {
30 | categoryArray = categoryArray.filter(i => i !== item);
31 | } else {
32 | categoryArray.push(item);
33 | }
34 |
35 | currentFilters[category] = categoryArray;
36 | this.setState({ filters: currentFilters }, () => this.ChangeUrl());
37 | trackEvent(`/filter/change/${category}`, currentFilters);
38 | }
39 |
40 | ChangeUrl() {
41 | var params = queryStringToParams(this.props.location.search);
42 | delete params["technology"];
43 | delete params["language"];
44 | delete params["solutionarea"];
45 | if (this.state.filters.technologies.length > 0) {
46 | params["technology"] = this.state.filters.technologies.join();
47 | }
48 | if (this.state.filters.languages.length > 0) {
49 | params["language"] = this.state.filters.languages.join();
50 | }
51 | if (this.state.filters.solutionareas.length > 0) {
52 | params["solutionarea"] = this.state.filters.solutionareas.join();
53 | }
54 |
55 | this.props.history.push(paramsToQueryString(params));
56 | }
57 |
58 | render() {
59 | const checkboxStyles = index => {
60 | return {
61 | root: {
62 | marginTop: index === 0 ? "9px" : "0px",
63 | marginBottom: "5px"
64 | }
65 | };
66 | };
67 |
68 | return (
69 |
70 |
Filter by
71 |
72 | Technology
73 | {Constants.technologies.map((technology, index) => (
74 |
80 | this.checkboxclicked(ev, checked, "technologies", technology)
81 | }
82 | />
83 | ))}
84 |
85 |
86 |
87 | Language
88 | {Constants.languages.map((language, index) => (
89 |
95 | this.checkboxclicked(ev, checked, "languages", language)
96 | }
97 | />
98 | ))}
99 |
100 |
101 |
102 | Solution Area
103 | {Constants.solutionAreas.map((solutionarea, index) => (
104 |
110 | this.checkboxclicked(ev, checked, "solutionareas", solutionarea)
111 | }
112 | />
113 | ))}
114 |
115 |
116 | );
117 | }
118 | }
119 |
120 | const mapStateToProps = state => ({});
121 |
122 | const mapDispatchToProps = {};
123 |
124 | const SideBarContainer = connect(
125 | mapStateToProps,
126 | mapDispatchToProps
127 | )(SideBar);
128 |
129 | export default withRouter(SideBarContainer);
130 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/appinsights.js:
--------------------------------------------------------------------------------
1 | export function trackEvent(eventName, eventData) {
2 | let appInsights = window.appInsights;
3 | if (typeof appInsights !== "undefined") {
4 | appInsights.trackEvent(eventName, eventData);
5 | }
6 | }
7 |
8 | export function trackError(errorString, properties) {
9 | let appInsights = window.appInsights;
10 | if (typeof appInsights !== "undefined") {
11 | appInsights.trackTrace(errorString, properties, 3);
12 | }
13 | }
14 |
15 | export function trackException(exception, properties) {
16 | let appInsights = window.appInsights;
17 | if (typeof appInsights !== "undefined") {
18 | appInsights.trackException(exception, null, properties);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/handle-response.js:
--------------------------------------------------------------------------------
1 | import { trackError, trackException } from "./appinsights";
2 |
3 | export function handleResponse(response) {
4 | try {
5 | return response.text().then(text => {
6 | if (response.ok) {
7 | return text;
8 | }
9 | const error = {
10 | status: response.status,
11 | error: text || response.statusText
12 | };
13 | trackError(error.error, { ...error, url: response.url });
14 | return Promise.reject(error);
15 | });
16 | } catch (ex) {
17 | trackException(ex, { url: response.url, method: "handleResponse" });
18 | return Promise.reject({
19 | status: -1,
20 | error: "Encountered unexpected exception."
21 | });
22 | }
23 | }
24 |
25 | export function handleJsonResponse(response) {
26 | return handleResponse(response).then(data => {
27 | try {
28 | return JSON.parse(data);
29 | } catch (ex) {
30 | trackException(ex, { url: response.url, method: "handleJsonResponse" });
31 | return Promise.reject({
32 | status: -1,
33 | error: "Encountered unexpected exception."
34 | });
35 | }
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from "./handle-response";
2 | export * from "./query-param";
3 | export * from "./appinsights";
4 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/query-param.js:
--------------------------------------------------------------------------------
1 | export function queryStringToParams(queryString) {
2 | if (queryString.indexOf("?") > -1) {
3 | queryString = queryString.split("?")[1];
4 | }
5 | var pairs = queryString.split("&");
6 | var result = {};
7 | pairs.forEach(function(pair) {
8 | pair = pair.split("=");
9 | if (pair[0] && pair[0].length > 0) {
10 | result[pair[0]] = decodeURIComponent(pair[1] || "");
11 | }
12 | });
13 | return result;
14 | }
15 |
16 | export function paramsToQueryString(params) {
17 | var queryString = Object.keys(params)
18 | .map(key => {
19 | return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
20 | })
21 | .join("&");
22 | return "?" + queryString;
23 | }
24 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/registerIcons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { registerIcons } from "office-ui-fabric-react";
3 | import "./registerIcons.scss";
4 | import { ReactComponent as GithubIconSvg } from "../assets/github.svg";
5 | import { ReactComponent as ContributionSvg } from "../assets/contribution.svg";
6 |
7 | export default function registerCustomIcons() {
8 | registerIcons({
9 | icons: {
10 | "GitHub-12px": ,
11 | "GitHub-16px": ,
12 | "contribution-svg":
13 | }
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/helpers/registerIcons.scss:
--------------------------------------------------------------------------------
1 | .icon-16px {
2 | width: 16px;
3 | height: 16px;
4 | vertical-align: baseline;
5 | fill: currentColor;
6 | }
7 |
8 | .icon-12px {
9 | width: 12px;
10 | height: 12px;
11 | vertical-align: baseline;
12 | margin-right: 4px;
13 | fill: currentColor;
14 | }
15 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | overflow: hidden;
6 | }
7 | html,
8 | body,
9 | #root,
10 | #container {
11 | height: 100%;
12 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/index.js:
--------------------------------------------------------------------------------
1 | // These must be the first lines in src/index.js
2 | import "react-app-polyfill/ie11";
3 | import "react-app-polyfill/stable";
4 |
5 | import "./index.css";
6 | import React from "react";
7 | import ReactDOM from "react-dom";
8 | import { BrowserRouter } from "react-router-dom";
9 | import { Provider } from "react-redux";
10 | import { initializeIcons } from "office-ui-fabric-react";
11 | import configureStore from "./reducers";
12 | import registerCustomIcons from "./helpers/registerIcons";
13 |
14 | import AppContainer from "./App";
15 |
16 | const store = configureStore();
17 | registerCustomIcons();
18 | initializeIcons();
19 | const rootElement = document.getElementById("root");
20 |
21 | ReactDOM.render(
22 |
23 |
24 |
25 |
26 | ,
27 | rootElement
28 | );
29 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/reducers/authenticationReducer.js:
--------------------------------------------------------------------------------
1 | import { userActionTypes } from "../actions/actionTypes";
2 | import initialState from "./initialState";
3 |
4 | export default function authenticationReducer(
5 | state = initialState.authentication,
6 | action
7 | ) {
8 | switch (action.type) {
9 | case userActionTypes.GETCURRENTUSER_REQUEST:
10 | return {
11 | ...state,
12 | loading: true
13 | };
14 | case userActionTypes.GETCURRENTUSER_SUCCESS:
15 | return {
16 | ...state,
17 | loading: false,
18 | loggedIn: true,
19 | user: action.user
20 | };
21 | case userActionTypes.GETCURRENTUSER_FAILURE:
22 | return {
23 | ...state,
24 | loading: false,
25 | loggedIn: false,
26 | user: {}
27 | };
28 | case userActionTypes.LOGOUT:
29 | return {
30 | ...state,
31 | loggedIn: false,
32 | user: {}
33 | };
34 | default:
35 | return state;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createStore } from "redux";
2 |
3 | import samples from "./sampleReducer";
4 | import authentication from "./authenticationReducer";
5 |
6 | const rootReducer = combineReducers({
7 | samples,
8 | authentication
9 | });
10 |
11 | export default function configureStore(initialState) {
12 | return createStore(rootReducer, initialState);
13 | }
14 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/reducers/initialState.js:
--------------------------------------------------------------------------------
1 | export default {
2 | samples: [],
3 | authentication: {
4 | loggedIn: false,
5 | user: {},
6 | loading: false
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/reducers/sampleReducer.js:
--------------------------------------------------------------------------------
1 | import { sampleActionTypes } from "../actions/actionTypes";
2 | import initialState from "./initialState";
3 |
4 | export default function sampleReducer(state = initialState.samples, action) {
5 | switch (action.type) {
6 | case sampleActionTypes.GETSAMPLES_SUCCESS:
7 | return action.samples;
8 | default:
9 | return state;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/services/github.service.js:
--------------------------------------------------------------------------------
1 | import { handleResponse } from "../helpers";
2 |
3 | export const githubService = {
4 | getReadMe,
5 | getLicense,
6 | getArmTemplate
7 | };
8 |
9 | function getRawContentUrl(repoUrl, fileName) {
10 | let rawUrl = repoUrl
11 | .replace("https://github.com", "https://raw.githubusercontent.com")
12 | .replace("/tree/", "/");
13 | rawUrl = rawUrl.includes("/master/") ? rawUrl + "/" : rawUrl + "/master/";
14 | let contentUrl = rawUrl + fileName;
15 | return contentUrl;
16 | }
17 |
18 | function getReadMe(repoUrl) {
19 | const requestOptions = {
20 | method: "GET"
21 | };
22 | const readMeUrl = getRawContentUrl(repoUrl, "README.md");
23 | return fetch(readMeUrl, requestOptions).then(handleResponse);
24 | }
25 |
26 | function getLicense(licenseUrl, repoUrl) {
27 | const requestOptions = {
28 | method: "GET"
29 | };
30 | const contentUrl = licenseUrl || getRawContentUrl(repoUrl, "LICENSE");
31 | return fetch(contentUrl, requestOptions).then(handleResponse);
32 | }
33 |
34 | function getArmTemplate(templateUrl) {
35 | const requestOptions = {
36 | method: "GET"
37 | };
38 | return fetch(templateUrl, requestOptions).then(handleResponse);
39 | }
40 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/services/index.js:
--------------------------------------------------------------------------------
1 | export * from "./user.service";
2 | export * from "./library.service";
3 | export * from "./github.service";
4 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/services/library.service.js:
--------------------------------------------------------------------------------
1 | import { handleResponse, handleJsonResponse } from "../helpers";
2 | import { trackException } from "../helpers/appinsights";
3 |
4 | export const libraryService = {
5 | getAllSamples,
6 | submitNewSample,
7 | updateUserSentimentStats,
8 | updateDownloadCount
9 | };
10 |
11 | function getAllSamples() {
12 | const requestOptions = {
13 | method: "GET"
14 | };
15 |
16 | return fetch("/api/Library", requestOptions).then(handleJsonResponse);
17 | }
18 |
19 | function submitNewSample(item) {
20 | const requestOptions = {
21 | method: "PUT",
22 | body: JSON.stringify(item),
23 | headers: {
24 | "Content-Type": "application/json"
25 | }
26 | };
27 | return fetch("/api/library", requestOptions)
28 | .then(handleJsonResponse)
29 | .catch(data => {
30 | let error = data.error;
31 | if (data.status === 400) {
32 | try {
33 | error = JSON.parse(data.error);
34 | } catch (ex) {
35 | trackException(ex, { method: "submitNewSample" });
36 | }
37 | }
38 | return Promise.reject({
39 | status: data.status,
40 | error: error
41 | });
42 | });
43 | }
44 |
45 | function updateUserSentimentStats(sentimentPayload) {
46 | const requestOptions = {
47 | method: "PUT",
48 | body: JSON.stringify(sentimentPayload),
49 | headers: {
50 | "Content-Type": "application/json"
51 | }
52 | };
53 | return fetch("/api/metrics/sentiment", requestOptions).then(handleResponse);
54 | }
55 |
56 | function updateDownloadCount(id) {
57 | const requestOptions = {
58 | method: "PUT",
59 | body: '"' + id + '"',
60 | headers: {
61 | "Content-Type": "application/json"
62 | }
63 | };
64 | return fetch("/api/metrics/downloads", requestOptions).then(handleResponse);
65 | }
66 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ClientApp/src/services/user.service.js:
--------------------------------------------------------------------------------
1 | import { handleResponse, handleJsonResponse } from "../helpers";
2 |
3 | export const userService = {
4 | getCurrentUser,
5 | logout
6 | };
7 |
8 | function getCurrentUser() {
9 | const requestOptions = {
10 | method: "GET"
11 | };
12 | return fetch("/api/user", requestOptions).then(handleJsonResponse);
13 | }
14 |
15 | function logout() {
16 | const requestOptions = {
17 | method: "GET"
18 | };
19 | return fetch("/api/user/logout", requestOptions).then(handleResponse);
20 | }
21 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Connected Services/Application Insights/ConnectedService.json:
--------------------------------------------------------------------------------
1 | {
2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
3 | "Version": "8.14.11009.1",
4 | "GettingStartedDocument": {
5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
6 | }
7 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Controllers/LibraryController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Text.RegularExpressions;
6 | using ServerlessLibrary.Models;
7 | using Newtonsoft.Json;
8 | using System.Threading.Tasks;
9 |
10 |
11 | // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
12 |
13 | namespace ServerlessLibrary.Controllers
14 | {
15 | [Produces("application/json")]
16 | [Route("api/[controller]")]
17 | public class LibraryController : Controller
18 | {
19 | ICacheService _cacheService;
20 | ILibraryStore _libraryStore;
21 |
22 | public LibraryController(ICacheService cacheService, ILibraryStore libraryStore)
23 | {
24 | this._cacheService = cacheService;
25 | this._libraryStore = libraryStore;
26 | }
27 |
28 | // GET: api/
29 | [HttpGet]
30 | [ProducesResponseType(typeof(IEnumerable), 200)]
31 | public JsonResult Get(string filterText, string language)
32 | {
33 | //TODO: Add filtering for solution areas and technologies.
34 | var results = _cacheService.GetCachedItems();
35 | var filteredResults = results.Where(
36 | x =>
37 | (
38 | (string.IsNullOrWhiteSpace(language) || x.Language == language) &&
39 | (
40 | string.IsNullOrWhiteSpace(filterText)
41 | || Regex.IsMatch(x.Title, filterText, RegexOptions.IgnoreCase)
42 | || Regex.IsMatch(x.Description, filterText, RegexOptions.IgnoreCase)
43 | || Regex.IsMatch(x.Repository.Replace("https://github.com/", "", StringComparison.InvariantCulture), filterText, RegexOptions.IgnoreCase)
44 | || (!string.IsNullOrWhiteSpace(x.Author) && Regex.IsMatch(x.Author, filterText, RegexOptions.IgnoreCase))
45 | || (x.Tags != null && x.Tags.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase)))
46 | || (x.Technologies != null && x.Technologies.Any(t => Regex.IsMatch(t, filterText, RegexOptions.IgnoreCase)))
47 | || (x.SolutionAreas != null && x.SolutionAreas.Any(c => Regex.IsMatch(c, filterText, RegexOptions.IgnoreCase)))
48 | )
49 | )
50 | );
51 |
52 | return new JsonResult(filteredResults);
53 | }
54 |
55 | [HttpPut]
56 | [ProducesResponseType(typeof(LibraryItem), 200)]
57 | public async Task Put([FromBody]LibraryItem libraryItem)
58 | {
59 | if (!User.Identity.IsAuthenticated)
60 | {
61 | return Unauthorized();
62 | }
63 |
64 | var validationsErrors = ValidateLibraryItem(libraryItem);
65 | if (validationsErrors?.Count > 0)
66 | {
67 | return BadRequest(validationsErrors);
68 | }
69 |
70 | // assign id, created date
71 | libraryItem.Id = Guid.NewGuid().ToString();
72 | libraryItem.CreatedDate = DateTime.UtcNow;
73 |
74 | // set the author to current authenticated user
75 | GitHubUser user = new GitHubUser(User);
76 | libraryItem.Author = user.UserName;
77 | await StorageHelper.submitContributionForApproval(JsonConvert.SerializeObject(libraryItem));
78 | return new JsonResult(libraryItem);
79 | }
80 |
81 | private static List ValidateLibraryItem(LibraryItem libraryItem)
82 | {
83 | List errors = new List();
84 | if (string.IsNullOrWhiteSpace(libraryItem.Title))
85 | {
86 | errors.Add("Title cannot be empty");
87 | }
88 |
89 | if (string.IsNullOrWhiteSpace(libraryItem.Repository) || !IsValidUri(libraryItem.Repository))
90 | {
91 | errors.Add("Repository URL must be a valid GitHub URL");
92 | }
93 |
94 | if (string.IsNullOrWhiteSpace(libraryItem.Description))
95 | {
96 | errors.Add("Description cannot be empty");
97 | }
98 |
99 | if (libraryItem.Technologies.Length == 0)
100 | {
101 | errors.Add("At least one technology must be specified");
102 | }
103 |
104 | if (string.IsNullOrWhiteSpace(libraryItem.Language))
105 | {
106 | errors.Add("Language must be specified");
107 | }
108 |
109 | if (libraryItem.SolutionAreas.Length == 0)
110 | {
111 | errors.Add("At least one solution area must be specified");
112 | }
113 |
114 | if (!string.IsNullOrWhiteSpace(libraryItem.Template) && !IsValidUri(libraryItem.Template, "raw.githubusercontent.com"))
115 | {
116 | errors.Add("ARM template URL must point to the raw path of the ARM template (https://raw.githubusercontent.com/...)");
117 | }
118 |
119 | return errors;
120 | }
121 |
122 | private static bool IsValidUri(string uriString, string expectedHostName = null)
123 | {
124 | try
125 | {
126 | var uri = new Uri(uriString);
127 | if (string.IsNullOrWhiteSpace(expectedHostName))
128 | {
129 | return true;
130 | }
131 | else
132 | {
133 | return string.Equals(expectedHostName, uri.Host, StringComparison.OrdinalIgnoreCase);
134 | }
135 | }
136 | catch
137 | {
138 | return false;
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Controllers/MetricsController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.Extensions.Logging;
3 | using Newtonsoft.Json;
4 | using ServerlessLibrary.Models;
5 | using System;
6 |
7 | namespace ServerlessLibrary.Controllers
8 | {
9 | [Route("api/[controller]/[action]")]
10 | [ApiController]
11 | public class MetricsController : ControllerBase
12 | {
13 | private readonly ILogger logger;
14 |
15 | public MetricsController(ILogger logger)
16 | {
17 | this.logger = logger;
18 | }
19 |
20 | // PUT api//downloads
21 | [ProducesResponseType(typeof(bool), 200)]
22 | [HttpPut]
23 | public JsonResult Downloads([FromBody]string id)
24 | {
25 | try
26 | {
27 | StorageHelper.updateUserStats(JsonConvert.SerializeObject(new { id, userAction = "download" })).Wait();
28 | }
29 | catch (Exception ex)
30 | {
31 | this.logger.LogError(ex, "Unable to update download count");
32 | }
33 |
34 | return new JsonResult(true);
35 | }
36 |
37 | // PUT api//sentiment
38 | [ProducesResponseType(typeof(bool), 200)]
39 | [HttpPut]
40 | public IActionResult Sentiment([FromBody]SentimentPayload sentimentPayload)
41 | {
42 | if (sentimentPayload.LikeChanges < -1
43 | || sentimentPayload.LikeChanges > 1
44 | || sentimentPayload.LikeChanges == sentimentPayload.DislikeChanges)
45 | {
46 | return BadRequest("Invalid values for like or dislike count");
47 | }
48 |
49 | try
50 | {
51 | StorageHelper.updateUserStats(JsonConvert.SerializeObject(new
52 | {
53 | id = sentimentPayload.Id,
54 | userAction = "Sentiment",
55 | likeChanges = sentimentPayload.LikeChanges,
56 | dislikeChanges = sentimentPayload.DislikeChanges
57 | })).Wait();
58 | }
59 | catch (Exception ex)
60 | {
61 | this.logger.LogError(ex, "Unable to update sentiments");
62 | }
63 |
64 | return new JsonResult(true);
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Controllers/UsersController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.AspNetCore.Authentication.Cookies;
3 | using Microsoft.AspNetCore.Mvc;
4 | using ServerlessLibrary.Models;
5 |
6 | namespace ServerlessLibrary.Controllers
7 | {
8 | [Route("api/[controller]")]
9 | [ApiController]
10 | public class UserController : ControllerBase
11 | {
12 | [HttpGet("login"), HttpPost("login")]
13 | public IActionResult Login(string returnUrl = "/")
14 | {
15 | if (User.Identity.IsAuthenticated)
16 | {
17 | return new RedirectResult(returnUrl);
18 | }
19 |
20 | // Instruct the middleware corresponding to the requested external identity
21 | // provider to redirect the user agent to its own authorization endpoint.
22 | // Note: the authenticationScheme parameter must match the value configured in Startup.cs.
23 | // If no scheme is provided then the DefaultChallengeScheme will be used
24 | return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
25 | }
26 |
27 | [HttpGet("logout"), HttpPost("logout")]
28 | public IActionResult Logout()
29 | {
30 | // Instruct the cookies middleware to delete the local cookie which
31 | // was created after a successful authentication flow.
32 | return SignOut(
33 | new AuthenticationProperties { RedirectUri = "/" },
34 | CookieAuthenticationDefaults.AuthenticationScheme);
35 | }
36 |
37 | [HttpGet]
38 | [ProducesResponseType(typeof(GitHubUser), 200)]
39 | public IActionResult Get()
40 | {
41 | if (User.Identity.IsAuthenticated)
42 | {
43 | GitHubUser user = new GitHubUser(User);
44 | return Ok(user);
45 | }
46 |
47 | return Unauthorized();
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/CosmosLibraryStore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Microsoft.Azure.Cosmos;
5 | using Microsoft.Azure.Cosmos.Fluent;
6 | using ServerlessLibrary.Models;
7 |
8 | namespace ServerlessLibrary
9 | {
10 | ///
11 | /// Cosmos db Library store
12 | ///
13 | public class CosmosLibraryStore : ILibraryStore
14 | {
15 | public CosmosLibraryStore()
16 | {
17 | CosmosDBRepository.Initialize();
18 | }
19 |
20 | public async Task Add(LibraryItem libraryItem)
21 | {
22 | await CosmosDBRepository.CreateItemAsync(libraryItem);
23 | }
24 |
25 | async public Task> GetAllItems()
26 | {
27 | IEnumerable libraryItems = await CosmosDBRepository.GetAllItemsAsync();
28 | return libraryItems.ToList();
29 | }
30 | }
31 |
32 | ///
33 | /// Cosmos db APIs
34 | ///
35 | ///
36 | static class CosmosDBRepository where T : class
37 | {
38 | private static readonly string DatabaseId = ServerlessLibrarySettings.Database;
39 | private static readonly string CollectionId = ServerlessLibrarySettings.Collection;
40 | private static Container container;
41 |
42 | public static async Task GetItemAsync(string id)
43 | {
44 | try
45 | {
46 | ItemResponse response = await container.ReadItemAsync(id, PartitionKey.None);
47 | return response.Resource;
48 | }
49 | catch (CosmosException e)
50 | {
51 | if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
52 | {
53 | return null;
54 | }
55 | else
56 | {
57 | throw;
58 | }
59 | }
60 | }
61 |
62 | public static async Task> GetAllItemsAsync()
63 | {
64 | FeedIterator query = container.GetItemQueryIterator(
65 | queryDefinition: null,
66 | requestOptions: new QueryRequestOptions() { MaxItemCount = -1 }); // NOTE: FeedOptions.EnableCrossPartitionQuery is removed in SDK v3 (https://docs.microsoft.com/en-us/azure/cosmos-db/sql/migrate-dotnet-v3?tabs=dotnet-v3#changes-to-feedoptions-queryrequestoptions-in-v30-sdk)
67 |
68 | List results = new List();
69 | using (query)
70 | {
71 | while (query.HasMoreResults)
72 | {
73 | results.AddRange(await query.ReadNextAsync());
74 | }
75 | }
76 |
77 | return results;
78 | }
79 |
80 | public static async Task CreateItemAsync(T item)
81 | {
82 | ItemResponse response = await container.CreateItemAsync(item, PartitionKey.None);
83 | return response.Resource;
84 | }
85 |
86 | public static async Task UpdateItemAsync(string id, T item)
87 | {
88 | ItemResponse response = await container.UpsertItemAsync(item, PartitionKey.None);
89 | return response.Resource;
90 | }
91 |
92 | public static async Task DeleteItemAsync(string id)
93 | {
94 | await container.DeleteItemAsync(id, PartitionKey.None);
95 | }
96 |
97 | public static void Initialize()
98 | {
99 | if (container == null)
100 | {
101 | CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(
102 | ServerlessLibrarySettings.CosmosEndpoint,
103 | ServerlessLibrarySettings.CosmosAuthkey);
104 | CosmosClient client = cosmosClientBuilder.Build();
105 |
106 | DatabaseResponse databaseResponse = client.CreateDatabaseIfNotExistsAsync(DatabaseId).Result;
107 | Database database = databaseResponse;
108 |
109 | ContainerResponse containerResponse = database.CreateContainerIfNotExistsAsync(id: CollectionId, partitionKeyPath: "/_partitionKey", throughput: 400).Result;
110 | container = containerResponse;
111 | }
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ILibraryStore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using ServerlessLibrary.Models;
4 |
5 | namespace ServerlessLibrary
6 | {
7 | ///
8 | /// Interface for serverless library store
9 | ///
10 | public interface ILibraryStore
11 | {
12 | ///
13 | /// Add an item to library
14 | ///
15 | /// Library item
16 | Task Add(LibraryItem libraryItem);
17 |
18 | ///
19 | /// Get all items from library
20 | ///
21 | ///
22 | Task> GetAllItems();
23 | }
24 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Models/GitHubUser.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants;
3 |
4 | namespace ServerlessLibrary.Models
5 | {
6 | public class GitHubUser
7 | {
8 | public GitHubUser()
9 | {
10 | }
11 |
12 | public GitHubUser(ClaimsPrincipal claimsPrincipal)
13 | {
14 | this.FullName = claimsPrincipal.FindFirstValue(Claims.Name);
15 | this.Email = claimsPrincipal.FindFirstValue(ClaimTypes.Email);
16 | this.AvatarUrl = claimsPrincipal.FindFirstValue(Claims.Avatar);
17 | this.UserName = claimsPrincipal.FindFirstValue(Claims.Login);
18 | }
19 |
20 | public string FullName { get; set; }
21 |
22 | public string Email { get; set; }
23 |
24 | public string AvatarUrl { get; set; }
25 |
26 | public string UserName { get; set; }
27 |
28 | public string DisplayName
29 | {
30 | get
31 | {
32 | if (!string.IsNullOrWhiteSpace(this.FullName))
33 | {
34 | return this.FullName.Split(' ')?[0];
35 | }
36 |
37 | if (!string.IsNullOrWhiteSpace(this.UserName))
38 | {
39 | return this.UserName;
40 | }
41 |
42 | return string.Empty;
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Models/LibraryItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 |
4 | namespace ServerlessLibrary.Models
5 | {
6 | public class LibraryItemWithStats : LibraryItem
7 | {
8 | [JsonProperty(PropertyName = "totaldownloads", DefaultValueHandling = DefaultValueHandling.Include)]
9 | public int TotalDownloads { get; set; }
10 |
11 | [JsonProperty(PropertyName = "likes", DefaultValueHandling = DefaultValueHandling.Include)]
12 | public int Likes { get; set; }
13 |
14 | [JsonProperty(PropertyName = "dislikes", DefaultValueHandling = DefaultValueHandling.Include)]
15 | public int Dislikes { get; set; }
16 | }
17 |
18 | public class LibraryItem
19 | {
20 | [JsonProperty(PropertyName = "id")]
21 | public string Id { get; set; }
22 |
23 | [JsonProperty(PropertyName = "createddate")]
24 | public DateTime CreatedDate { get; set; }
25 |
26 | [JsonProperty(PropertyName = "title", DefaultValueHandling = DefaultValueHandling.Include)]
27 | public string Title { get; set; }
28 |
29 | [JsonProperty(PropertyName = "template", DefaultValueHandling = DefaultValueHandling.Include)]
30 | public string Template { get; set; }
31 |
32 | [JsonProperty(PropertyName = "repository", DefaultValueHandling = DefaultValueHandling.Include)]
33 | public string Repository { get; set; }
34 |
35 | [JsonProperty(PropertyName = "description", DefaultValueHandling = DefaultValueHandling.Include)]
36 | public string Description { get; set; }
37 |
38 | [JsonProperty(PropertyName = "tags", DefaultValueHandling = DefaultValueHandling.Include)]
39 | public string[] Tags { get; set; }
40 |
41 | [JsonProperty(PropertyName = "language", DefaultValueHandling = DefaultValueHandling.Include)]
42 | public string Language { get; set; }
43 |
44 | [JsonProperty(PropertyName = "technologies", DefaultValueHandling = DefaultValueHandling.Include)]
45 | public string[] Technologies { get; set; }
46 |
47 | [JsonProperty(PropertyName = "solutionareas", DefaultValueHandling = DefaultValueHandling.Include)]
48 | public string[] SolutionAreas { get; set; }
49 |
50 | [JsonProperty(PropertyName = "author", DefaultValueHandling = DefaultValueHandling.Include)]
51 | public string Author { get; internal set; }
52 |
53 | internal T ConvertTo()
54 | {
55 | var serializedObj = JsonConvert.SerializeObject(this);
56 | return JsonConvert.DeserializeObject(serializedObj);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Models/SentimentPayload.cs:
--------------------------------------------------------------------------------
1 | namespace ServerlessLibrary.Models
2 | {
3 | public class SentimentPayload
4 | {
5 | public string Id { get; set; }
6 | public int LikeChanges { get; set; }
7 | public int DislikeChanges { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationConstants.cs:
--------------------------------------------------------------------------------
1 | namespace ServerlessLibrary.OAuth.GitHub
2 | {
3 | ///
4 | /// Contains constants specific to the .
5 | ///
6 | public static class GitHubAuthenticationConstants
7 | {
8 | public static class Claims
9 | {
10 | public const string Name = "urn:github:name";
11 | public const string Url = "urn:github:url";
12 | public const string Login = "urn:github:login";
13 | public const string Avatar = "urn:github:avatar";
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | namespace ServerlessLibrary.OAuth.GitHub
2 | {
3 | ///
4 | /// Default values used by the GitHub authentication middleware.
5 | ///
6 | public static class GitHubAuthenticationDefaults
7 | {
8 | ///
9 | /// Default value for .
10 | ///
11 | public const string AuthenticationScheme = "GitHub";
12 |
13 | ///
14 | /// Default value for .
15 | ///
16 | public const string DisplayName = "GitHub";
17 |
18 | ///
19 | /// Default value for .
20 | ///
21 | public const string Issuer = "GitHub";
22 |
23 | ///
24 | /// Default value for .
25 | ///
26 | public const string CallbackPath = "/signin-github";
27 |
28 | ///
29 | /// Default value for .
30 | ///
31 | public const string AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
32 |
33 | ///
34 | /// Default value for .
35 | ///
36 | public const string TokenEndpoint = "https://github.com/login/oauth/access_token";
37 |
38 | ///
39 | /// Default value for .
40 | ///
41 | public const string UserInformationEndpoint = "https://api.github.com/user";
42 |
43 | ///
44 | /// Default value for .
45 | ///
46 | public const string UserEmailsEndpoint = "https://api.github.com/user/emails";
47 |
48 | ///
49 | /// Scope for .
50 | ///
51 | public const string UserInformationScope = "user";
52 |
53 | ///
54 | /// Scope for .
55 | ///
56 | public const string UserEmailsScope = "user:email";
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Security.Claims;
5 | using System.Text.Encodings.Web;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Authentication;
8 | using Microsoft.AspNetCore.Authentication.OAuth;
9 | using Microsoft.Extensions.Logging;
10 | using Microsoft.Extensions.Options;
11 | using Newtonsoft.Json.Linq;
12 |
13 | namespace ServerlessLibrary.OAuth.GitHub
14 | {
15 | public class GitHubAuthenticationHandler : OAuthHandler
16 | {
17 | public GitHubAuthenticationHandler(
18 | IOptionsMonitor options,
19 | ILoggerFactory logger,
20 | UrlEncoder encoder,
21 | ISystemClock clock)
22 | : base(options, logger, encoder, clock)
23 | {
24 | }
25 |
26 | protected override async Task CreateTicketAsync(ClaimsIdentity identity,
27 | AuthenticationProperties properties, OAuthTokenResponse tokens)
28 | {
29 | var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
30 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
31 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
32 |
33 | var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
34 | if (!response.IsSuccessStatusCode)
35 | {
36 | Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
37 | "returned a {Status} response with the following payload: {Headers} {Body}.",
38 | /* Status: */ response.StatusCode,
39 | /* Headers: */ response.Headers.ToString(),
40 | /* Body: */ await response.Content.ReadAsStringAsync());
41 |
42 | throw new HttpRequestException("An error occurred while retrieving the user profile.");
43 | }
44 |
45 | var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
46 |
47 | var principal = new ClaimsPrincipal(identity);
48 | var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload);
49 |
50 | context.RunClaimActions(payload);
51 |
52 | // When the email address is not public, retrieve it from
53 | // the emails endpoint if the user:email scope is specified.
54 | if (!string.IsNullOrEmpty(Options.UserEmailsEndpoint) &&
55 | !identity.HasClaim(claim => claim.Type == ClaimTypes.Email) && Options.Scope.Contains("user:email"))
56 | {
57 | var address = await GetEmailAsync(tokens);
58 | if (!string.IsNullOrEmpty(address))
59 | {
60 | identity.AddClaim(new Claim(ClaimTypes.Email, address, ClaimValueTypes.String, Options.ClaimsIssuer));
61 | }
62 | }
63 |
64 | await Options.Events.CreatingTicket(context);
65 | return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
66 | }
67 |
68 | protected virtual async Task GetEmailAsync(OAuthTokenResponse tokens)
69 | {
70 | // See https://developer.github.com/v3/users/emails/ for more information about the /user/emails endpoint.
71 | var request = new HttpRequestMessage(HttpMethod.Get, Options.UserEmailsEndpoint);
72 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
73 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
74 |
75 | // Failed requests shouldn't cause an error: in this case, return null to indicate that the email address cannot be retrieved.
76 | var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
77 | if (!response.IsSuccessStatusCode)
78 | {
79 | Logger.LogWarning("An error occurred while retrieving the email address associated with the logged in user: " +
80 | "the remote server returned a {Status} response with the following payload: {Headers} {Body}.",
81 | /* Status: */ response.StatusCode,
82 | /* Headers: */ response.Headers.ToString(),
83 | /* Body: */ await response.Content.ReadAsStringAsync());
84 |
85 | return null;
86 | }
87 |
88 | var payload = JArray.Parse(await response.Content.ReadAsStringAsync());
89 |
90 | return (from address in payload.AsJEnumerable()
91 | where address.Value("primary")
92 | select address.Value("email")).FirstOrDefault();
93 | }
94 |
95 | protected override Task HandleAuthenticateAsync()
96 | {
97 | return base.HandleAuthenticateAsync();
98 | }
99 |
100 | protected override Task HandleRemoteAuthenticateAsync()
101 | {
102 | return base.HandleRemoteAuthenticateAsync();
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/OAuth.GitHub/GitHubAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using Microsoft.AspNetCore.Authentication;
3 | using Microsoft.AspNetCore.Authentication.OAuth;
4 | using Microsoft.AspNetCore.Http;
5 | using static ServerlessLibrary.OAuth.GitHub.GitHubAuthenticationConstants;
6 |
7 | namespace ServerlessLibrary.OAuth.GitHub
8 | {
9 | ///
10 | /// Defines a set of options used by .
11 | ///
12 | public class GitHubAuthenticationOptions : OAuthOptions
13 | {
14 | public GitHubAuthenticationOptions()
15 | {
16 | ClaimsIssuer = GitHubAuthenticationDefaults.Issuer;
17 |
18 | CallbackPath = new PathString(GitHubAuthenticationDefaults.CallbackPath);
19 |
20 | AuthorizationEndpoint = GitHubAuthenticationDefaults.AuthorizationEndpoint;
21 | TokenEndpoint = GitHubAuthenticationDefaults.TokenEndpoint;
22 | UserInformationEndpoint = GitHubAuthenticationDefaults.UserInformationEndpoint;
23 | //Scope.Add(GitHubAuthenticationDefaults.UserInformationScope);
24 | Scope.Add(GitHubAuthenticationDefaults.UserEmailsScope);
25 |
26 | ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
27 | ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
28 | ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
29 | ClaimActions.MapJsonKey(Claims.Name, "name");
30 | ClaimActions.MapJsonKey(Claims.Url, "html_url");
31 | ClaimActions.MapJsonKey(Claims.Login, "login");
32 | ClaimActions.MapJsonKey(Claims.Avatar, "avatar_url");
33 | }
34 |
35 | ///
36 | /// Gets or sets the address of the endpoint exposing
37 | /// the email addresses associated with the logged in user.
38 | ///
39 | public string UserEmailsEndpoint { get; set; } = GitHubAuthenticationDefaults.UserEmailsEndpoint;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Pages/Error.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model ErrorModel
3 | @{
4 | ViewData["Title"] = "Error";
5 | }
6 |
7 | Error.
8 | An error occurred while processing your request.
9 |
10 | @if (Model.ShowRequestId)
11 | {
12 |
13 | Request ID: @Model.RequestId
14 |
15 | }
16 |
17 | Development Mode
18 |
19 | Swapping to Development environment will display more detailed information about the error that occurred.
20 |
21 |
22 | Development environment should not be enabled in deployed applications , as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development , and restarting the application.
23 |
24 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Pages/Error.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.AspNetCore.Mvc.RazorPages;
8 |
9 | namespace ServerlessLibraryAPI.Pages
10 | {
11 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
12 | public class ErrorModel : PageModel
13 | {
14 | public string RequestId { get; set; }
15 |
16 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
17 |
18 | public void OnGet()
19 | {
20 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using ServerlessLibraryAPI
2 | @namespace ServerlessLibraryAPI.Pages
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Logging.ApplicationInsights;
5 |
6 | namespace ServerlessLibrary
7 | {
8 | public class Program
9 | {
10 | public static void Main(string[] args)
11 | {
12 | CreateWebHostBuilder(args).Build().Run();
13 | }
14 |
15 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
16 | WebHost.CreateDefaultBuilder(args)
17 | .UseApplicationInsights()
18 | .UseStartup()
19 | .ConfigureLogging(
20 | builder =>
21 | {
22 | builder.AddApplicationInsights();
23 | builder.AddFilter("ServerlessLibrary.Program", LogLevel.Information);
24 | builder.AddFilter("ServerlessLibrary.Startup", LogLevel.Information);
25 | builder.AddFilter("", LogLevel.Information);
26 | }
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:16743/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "IIS Express - API only": {
19 | "commandName": "IISExpress",
20 | "launchBrowser": true,
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development",
23 | "ApiOnly": "true"
24 | }
25 | },
26 | "ServerLessLibrary": {
27 | "commandName": "Project",
28 | "launchBrowser": true,
29 | "environmentVariables": {
30 | "ASPNETCORE_ENVIRONMENT": "Development"
31 | },
32 | "applicationUrl": "http://localhost:16744/"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ServerlessLibraryAPI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.1
5 | true
6 | Latest
7 | false
8 | ClientApp\
9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\**
10 | /subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary
11 | /subscriptions/7c1b7bab-00b2-4cb7-924e-205c4f411810/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/ServerlessLibrary
12 | 235c2497-239d-47f0-8ea7-af2dd2416d95
13 | ServerlessLibrary
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | %(DistFiles.Identity)
57 | PreserveNewest
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ServerlessLibraryAPI.csproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FolderProfile
5 | true
6 |
7 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/ServerlessLibrarySettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using System.Threading.Tasks;
7 |
8 | namespace ServerlessLibrary
9 | {
10 | public static class ServerlessLibrarySettings
11 | {
12 | private static string config(string @default = null, [CallerMemberName] string key = null)
13 | {
14 | var value = System.Environment.GetEnvironmentVariable(key) ?? ConfigurationManager.AppSettings[key];
15 | return string.IsNullOrEmpty(value)
16 | ? @default
17 | : value;
18 | }
19 |
20 | public static string SLStorageString { get { return config("UseDevelopmentStorage=true"); } }
21 | public static string SLAppInsightsKey { get { return config(""); } }
22 | public static int SLCacheRefreshIntervalInSeconds { get { return Int32.Parse(config("60")); } }
23 | public static string CACHE_ENTRY = "_CacheEntry";
24 | public static string CosmosEndpoint { get { return config(); } }
25 | public static string CosmosAuthkey { get { return config(); } }
26 | public static string Database { get { return "serverlesslibrary"; } }
27 | public static string Collection { get { return "contributions"; } }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication.Cookies;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Logging;
10 | using ServerlessLibrary.OAuth.GitHub;
11 | using System.Threading.Tasks;
12 |
13 | namespace ServerlessLibrary
14 | {
15 | public class Startup
16 | {
17 | public Startup(IConfiguration configuration)
18 | {
19 | Configuration = configuration;
20 | }
21 |
22 | public IConfiguration Configuration { get; }
23 |
24 | // This method gets called by the runtime. Use this method to add services to the container.
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddMemoryCache();
28 |
29 | services.AddAuthentication(options =>
30 | {
31 | options.DefaultChallengeScheme = GitHubAuthenticationDefaults.AuthenticationScheme;
32 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
33 | })
34 | .AddCookie()
35 | .AddOAuth(
36 | GitHubAuthenticationDefaults.AuthenticationScheme,
37 | GitHubAuthenticationDefaults.DisplayName,
38 | options =>
39 | {
40 | options.ClientId = Configuration["Authentication:GitHub:ClientId"]; // these settings need to be present in appSettings (or in secrets.json)
41 | options.ClientSecret = Configuration["Authentication:GitHub:ClientSecret"];
42 | });
43 |
44 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
45 |
46 | // In production, the React files will be served from this directory
47 | services.AddSpaStaticFiles(configuration =>
48 | {
49 | configuration.RootPath = "ClientApp/build";
50 | });
51 |
52 | // ToDo: re-enable swagger
53 | //services.AddSwaggerGen(c =>
54 | //{
55 | // c.SwaggerDoc("v1", new Info
56 | // {
57 | // Title = "ASP.NET Core 2.0 Web API",
58 | // Version = "v1"
59 | // });
60 | //});
61 |
62 | services.AddSingleton();
63 | services.AddSingleton();
64 | }
65 |
66 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
67 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
68 | {
69 | app.UseHsts();
70 | app.UseHttpsRedirection();
71 | app.UseDefaultFiles();
72 | app.UseStaticFiles();
73 | app.UseSpaStaticFiles();
74 |
75 | // ToDo: Re-enable swagger
76 | //app.UseSwaggerUI(c =>
77 | //{
78 | // c.SwaggerEndpoint("/swagger/v1/swagger.json", "Serverless library API v1");
79 | // c.RoutePrefix = "swagger";
80 | //});
81 |
82 | app.UseAuthentication();
83 |
84 | app.UseMvc(routes =>
85 | {
86 | routes.MapRoute(
87 | name: "default",
88 | template: "{controller}/{action=Index}/{id?}");
89 | });
90 |
91 | app.UseSpa(spa =>
92 | {
93 | spa.Options.SourcePath = "ClientApp";
94 |
95 | if (env.IsDevelopment())
96 | {
97 | spa.UseReactDevelopmentServer(npmScript: "start");
98 | }
99 | });
100 |
101 | app.Use(async (context, next) =>
102 | {
103 | context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
104 | context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
105 | context.Response.Headers.Add("Strict-Transport-Security", "max-age=600; includeSubDomains; preload");
106 | context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
107 | await next();
108 | });
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/StorageHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Microsoft.WindowsAzure.Storage;
5 | using Microsoft.WindowsAzure.Storage.Queue;
6 | using Microsoft.WindowsAzure.Storage.RetryPolicies;
7 | using Microsoft.WindowsAzure.Storage.Table;
8 |
9 | namespace ServerlessLibrary
10 | {
11 | ///
12 | /// Summary description for StorageHelper
13 | ///
14 | public class StorageHelper
15 | {
16 | private const string slItemTableName = "slitemstats";
17 | private const string slContributionRequests = "contribution-requests";
18 | private static readonly TableRequestOptions tableRequestRetry =
19 | new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) };
20 |
21 | private static CloudTableClient tableClient()
22 | {
23 | // Retrieve storage account from connection string.
24 | CloudStorageAccount storageAccount =
25 | CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
26 |
27 | // Create the table client.
28 | return storageAccount.CreateCloudTableClient();
29 | }
30 |
31 | private static CloudQueueClient cloudQueueClient()
32 | {
33 | // Retrieve storage account from connection string.
34 | CloudStorageAccount storageAccount =
35 | CloudStorageAccount.Parse(ServerlessLibrarySettings.SLStorageString);
36 |
37 | // Create the queue client.
38 | return storageAccount.CreateCloudQueueClient();
39 | }
40 |
41 | private static async Task getTableReference(string tableName = slItemTableName)
42 | {
43 | CloudTable table = tableClient().GetTableReference(tableName);
44 | await table.CreateIfNotExistsAsync();
45 | return table;
46 | }
47 |
48 | private static async Task getQueueReference(string queueName)
49 | {
50 | CloudQueue queue = cloudQueueClient().GetQueueReference(queueName);
51 | await queue.CreateIfNotExistsAsync();
52 | return queue;
53 | }
54 |
55 | public static async Task submitContributionForApproval(string contributionPayload)
56 | {
57 | var message = new CloudQueueMessage(contributionPayload);
58 | await (await getQueueReference(slContributionRequests)).AddMessageAsync(message);
59 | }
60 |
61 | public static async Task updateUserStats(string statsPayload)
62 | {
63 | var message = new CloudQueueMessage(statsPayload);
64 | await (await getQueueReference(slItemTableName)).AddMessageAsync(message);
65 | }
66 |
67 | public static async Task> getSLItemRecordsAsync()
68 | {
69 | TableQuery query = new TableQuery()
70 | .Select(new List { "id", "totalDownloads", "likes", "dislikes" });
71 | TableContinuationToken continuationToken = null;
72 | List entities = new List();
73 | var opContext = new OperationContext();
74 | do
75 | {
76 | TableQuerySegment queryResults =
77 | await (await getTableReference()).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext);
78 | continuationToken = queryResults.ContinuationToken;
79 | entities.AddRange(queryResults.Results);
80 |
81 | } while (continuationToken != null);
82 | return entities;
83 | }
84 |
85 | public async Task GetItem(string id)
86 | {
87 | TableOperation operation = TableOperation.Retrieve(id, id);
88 |
89 | TableResult result = await (await getTableReference()).ExecuteAsync(operation);
90 |
91 | return (SLItemStats)(dynamic)result.Result;
92 | }
93 |
94 | }
95 |
96 | public class SLItemStats : TableEntity
97 | {
98 | public string id { get; set; }
99 | public int totalDownloads { get; set; }
100 | public DateTime lastUpdated { get; set; }
101 | public int likes { get; set; }
102 | public int dislikes { get; set; }
103 |
104 | }
105 |
106 | }
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | },
9 | "ApplicationInsights": {
10 | "InstrumentationKey": ""
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ServerlessLibraryAPI/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*",
8 | "ApplicationInsights": {
9 | "InstrumentationKey": "d35b5caf-a276-467c-9ac7-f7f7d84ea171"
10 | },
11 | "Authentication": {
12 | "GitHub": {
13 | "ClientId": "",
14 | "ClientSecret": ""
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/ServerlessLibraryFunctionApp/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # Azure Functions localsettings file
5 | local.settings.json
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
--------------------------------------------------------------------------------
/ServerlessLibraryFunctionApp/Properties/PublishProfiles/slfunctionapp - Web Deploy.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | $slfunctionapp
9 | MSDeploy
10 | AzureWebSite
11 | Release
12 | Any CPU
13 | http://slfunctionapp.azurewebsites.net
14 | False
15 | False
16 | slfunctionapp.scm.azurewebsites.net:443
17 | slfunctionapp
18 | True
19 | WMSVC
20 | True
21 | <_SavePWD>True
22 | False
23 | /subscriptions/499a4934-1b59-43e4-9472-f5a8f6309fea/resourceGroups/ServerlessLibrary/providers/Microsoft.Web/sites/slfunctionapp
24 |
25 |
--------------------------------------------------------------------------------
/ServerlessLibraryFunctionApp/ServerLessLibraryFunctionApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | v3
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 | PreserveNewest
18 | Never
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ServerlessLibraryFunctionApp/UpdateCounts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Microsoft.Azure.WebJobs;
5 | using Microsoft.Extensions.Logging;
6 | using Microsoft.WindowsAzure.Storage;
7 | using Microsoft.WindowsAzure.Storage.RetryPolicies;
8 | using Microsoft.WindowsAzure.Storage.Table;
9 | using Newtonsoft.Json;
10 |
11 | namespace ServerlessLibraryFunctionApp
12 | {
13 | public static class UpdateCounts
14 | {
15 | private static readonly TableRequestOptions tableRequestRetry = new TableRequestOptions { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(2), 3) };
16 |
17 | [FunctionName("UpdateCounts")]
18 | [Singleton]
19 | public static async Task Run([QueueTrigger("slitemstats")]string myQueueItemJson, [Table("slitemstats")] CloudTable table, ILogger log)
20 | {
21 | var payload = JsonConvert.DeserializeObject(((dynamic)myQueueItemJson));
22 | string id = payload.id.ToString();
23 |
24 | var userActionString = payload.userAction.ToString();
25 | log.LogInformation($"Id:{id}, UserAction: {userActionString}");
26 | UserAction userAction;
27 | if (!Enum.TryParse(userActionString, true, out userAction))
28 | {
29 | log.LogInformation($"Unknown user action received.");
30 | return;
31 | }
32 |
33 | int likeChanges = 0, dislikeChanges = 0;
34 | if (userAction == UserAction.Sentiment)
35 | {
36 | try
37 | {
38 | likeChanges = (int)payload.likeChanges;
39 | dislikeChanges = (int)payload.dislikeChanges;
40 | }
41 | catch (Exception ex)
42 | {
43 | log.LogInformation($"Exception got in casting {ex}");
44 | }
45 | }
46 |
47 | string mainFilter = TableQuery.GenerateFilterCondition("id", QueryComparisons.Equal, id);
48 | TableQuery query = new TableQuery().Where(mainFilter);
49 | TableContinuationToken continuationToken = null;
50 | List entities = new List();
51 | var opContext = new OperationContext();
52 | do
53 | {
54 | TableQuerySegment
55 | queryResults = await (table).ExecuteQuerySegmentedAsync(query, continuationToken, tableRequestRetry, opContext);
56 | continuationToken = queryResults.ContinuationToken;
57 | entities.AddRange(queryResults.Results);
58 |
59 | } while (continuationToken != null);
60 | if (entities.Count == 0)
61 | {
62 | // Create new entry
63 | SLItemStats item = null;
64 | item = new SLItemStats()
65 | {
66 | PartitionKey = Guid.NewGuid().ToString(),
67 | RowKey = Guid.NewGuid().ToString(),
68 | id = id,
69 | totalDownloads = userAction == UserAction.Download ? 1 : 0,
70 | lastUpdated = DateTime.UtcNow,
71 | likes = likeChanges,
72 | dislikes = dislikeChanges
73 | };
74 |
75 | // Create the TableOperation that inserts the itemStats entity.
76 | TableOperation insertOperation = TableOperation.Insert(item);
77 |
78 | // Execute the insert operation.
79 | await table.ExecuteAsync(insertOperation);
80 |
81 | }
82 | else
83 | {
84 | //Update existing entry
85 | var item = entities[0];
86 | switch (userAction)
87 | {
88 | case UserAction.Download:
89 | item.totalDownloads += 1;
90 | break;
91 | case UserAction.Sentiment:
92 | item.likes += likeChanges;
93 | item.dislikes += dislikeChanges;
94 | break;
95 | default:
96 | log.LogInformation($"Unexpected user action.");
97 | return;
98 | }
99 |
100 | TableOperation operation = TableOperation.InsertOrMerge(item);
101 | await table.ExecuteAsync(operation);
102 | }
103 | }
104 | }
105 |
106 | enum UserAction
107 | {
108 | Download,
109 | Sentiment
110 | }
111 |
112 | public class SLItemStats : TableEntity
113 | {
114 | public string id { get; set; }
115 | public int totalDownloads { get; set; }
116 | public DateTime lastUpdated { get; set; }
117 | public int likes { get; set; }
118 | public int dislikes { get; set; }
119 |
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/ServerlessLibraryFunctionApp/host.json:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/ServerlessLibraryLogicApp/Deploy-AzureResourceGroup.ps1:
--------------------------------------------------------------------------------
1 | #Requires -Version 3.0
2 |
3 | Param(
4 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation,
5 | [string] $ResourceGroupName = 'ServerlessLibrary',
6 | [switch] $UploadArtifacts,
7 | [string] $StorageAccountName,
8 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
9 | [string] $TemplateFile = 'LogicApp.json',
10 | [string] $TemplateParametersFile = 'LogicApp.parameters.json',
11 | [string] $ArtifactStagingDirectory = '.',
12 | [string] $DSCSourceFolder = 'DSC',
13 | [switch] $ValidateOnly
14 | )
15 |
16 | try {
17 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0')
18 | } catch { }
19 |
20 | $ErrorActionPreference = 'Stop'
21 | Set-StrictMode -Version 3
22 |
23 | function Format-ValidationOutput {
24 | param ($ValidationOutput, [int] $Depth = 0)
25 | Set-StrictMode -Off
26 | return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) })
27 | }
28 |
29 | $OptionalParameters = New-Object -TypeName Hashtable
30 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile))
31 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile))
32 |
33 | if ($UploadArtifacts) {
34 | # Convert relative paths to absolute paths if needed
35 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory))
36 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder))
37 |
38 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
39 | $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
40 | if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) {
41 | $JsonParameters = $JsonParameters.parameters
42 | }
43 | $ArtifactsLocationName = '_artifactsLocation'
44 | $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken'
45 | $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore
46 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore
47 |
48 | # Create DSC configuration archive
49 | if (Test-Path $DSCSourceFolder) {
50 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName})
51 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
52 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
53 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
54 | }
55 | }
56 |
57 | # Create a storage account name if none was provided
58 | if ($StorageAccountName -eq '') {
59 | $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19)
60 | }
61 |
62 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName})
63 |
64 | # Create the storage account if it doesn't already exist
65 | if ($StorageAccount -eq $null) {
66 | $StorageResourceGroupName = 'ARM_Deploy_Staging'
67 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force
68 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation"
69 | }
70 |
71 | # Generate the value for artifacts location if it is not provided in the parameter file
72 | if ($OptionalParameters[$ArtifactsLocationName] -eq $null) {
73 | $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName
74 | }
75 |
76 | # Copy files from the local storage staging location to the storage account container
77 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1
78 |
79 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName}
80 | foreach ($SourcePath in $ArtifactFilePaths) {
81 | Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) `
82 | -Container $StorageContainerName -Context $StorageAccount.Context -Force
83 | }
84 |
85 | # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file
86 | if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) {
87 | $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force `
88 | (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4))
89 | }
90 | }
91 |
92 | # Create the resource group only when it doesn't already exist
93 | if ((Get-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) {
94 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop
95 | }
96 |
97 | if ($ValidateOnly) {
98 | $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName `
99 | -TemplateFile $TemplateFile `
100 | -TemplateParameterFile $TemplateParametersFile `
101 | @OptionalParameters)
102 | if ($ErrorMessages) {
103 | Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.'
104 | }
105 | else {
106 | Write-Output '', 'Template is valid.'
107 | }
108 | }
109 | else {
110 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
111 | -ResourceGroupName $ResourceGroupName `
112 | -TemplateFile $TemplateFile `
113 | -TemplateParameterFile $TemplateParametersFile `
114 | @OptionalParameters `
115 | -Force -Verbose `
116 | -ErrorVariable ErrorMessages
117 | if ($ErrorMessages) {
118 | Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") })
119 | }
120 | }
--------------------------------------------------------------------------------
/ServerlessLibraryLogicApp/Deployment.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | bin\$(Configuration)\
7 | false
8 | true
9 | false
10 | None
11 | obj\
12 | $(BaseIntermediateOutputPath)\
13 | $(BaseIntermediateOutputPath)$(Configuration)\
14 | $(IntermediateOutputPath)ProjectReferences
15 | $(ProjectReferencesOutputPath)\
16 | true
17 |
18 |
19 |
20 | false
21 | false
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Always
33 |
34 |
35 | Never
36 |
37 |
38 | false
39 | Build
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | _GetDeploymentProjectContent;
48 | _CalculateContentOutputRelativePaths;
49 | _GetReferencedProjectsOutput;
50 | _CalculateArtifactStagingDirectory;
51 | _CopyOutputToArtifactStagingDirectory;
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Configuration=$(Configuration);Platform=$(Platform)
69 |
70 |
71 |
75 |
76 |
77 |
78 | $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)'))
79 |
80 |
81 |
82 |
83 |
84 |
85 | $(OutDir)
86 | $(OutputPath)
87 | $(ArtifactStagingDirectory)\
88 | $(ArtifactStagingDirectory)staging\
89 | $(Build_StagingDirectory)
90 |
91 |
92 |
93 |
94 |
96 |
97 | <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity)
98 | <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', ''))
99 |
100 |
101 |
102 |
103 | $(_RelativePath)
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | PrepareForRun
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/ServerlessLibraryLogicApp/LogicApp.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "logicAppName": {
6 | "value": "ApprovalWorkflow"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/ServerlessLibraryLogicApp/ServerlessLibraryLogicApp.deployproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 |
8 |
9 | Release
10 | AnyCPU
11 |
12 |
13 |
14 | 92dd9a5d-3a93-493c-8f69-23a1c519a1c4
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | False
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------