mirror of
https://github.com/zhigang1992/apollo.git
synced 2026-05-13 00:49:02 +08:00
Merge branch 'master' into chang/2-links-to-whats-new
This commit is contained in:
@@ -34,7 +34,6 @@ module.exports = {
|
||||
'platform/operation-registry',
|
||||
'platform/editor-plugins',
|
||||
'platform/tracing',
|
||||
'platform/setup-analytics',
|
||||
'platform/errors',
|
||||
'platform/integrations'
|
||||
],
|
||||
@@ -48,6 +47,7 @@ module.exports = {
|
||||
],
|
||||
References: [
|
||||
'references/apollo-config',
|
||||
'references/setup-analytics',
|
||||
'references/apollo-engine',
|
||||
'references/engine-proxy',
|
||||
'references/engine-proxy-release-notes'
|
||||
|
||||
10
docs/package-lock.json
generated
10
docs/package-lock.json
generated
@@ -4306,7 +4306,7 @@
|
||||
},
|
||||
"css-select": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||
"requires": {
|
||||
"boolbase": "~1.0.0",
|
||||
@@ -10249,7 +10249,7 @@
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
|
||||
},
|
||||
"wordwrap": {
|
||||
@@ -10574,7 +10574,7 @@
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
||||
},
|
||||
"pinkie": {
|
||||
@@ -12406,7 +12406,7 @@
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
||||
"requires": {
|
||||
"ret": "~0.1.10"
|
||||
@@ -13652,7 +13652,7 @@
|
||||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"through2": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: "2. Hook up your data sources"
|
||||
title: '2. Hook up your data sources'
|
||||
description: Connect REST and SQL data to your graph
|
||||
---
|
||||
|
||||
Time to accomplish: _10 Minutes_
|
||||
|
||||
Now that we've constructed our schema, we need to hook up our data sources to our GraphQL API. GraphQL APIs are extremely flexible, because you can layer them on top of any service, including any business logic, REST APIs, databases, or gRPC services.
|
||||
Now that we've constructed our schema, we need to hook up our data sources to our GraphQL API. GraphQL APIs are extremely flexible because you can layer them on top of any service, including any business logic, REST APIs, databases, or gRPC services.
|
||||
|
||||
Apollo makes connecting these services to your graph simple with our data source API. An **Apollo data source** is a class that encapsulates all of the data fetching logic, as well as caching and deduplication, for a particular service. By using Apollo data sources to hook up your services to your graph API, you're also following best practices for organizing your code.
|
||||
|
||||
@@ -120,7 +120,7 @@ Now that we've connected our REST API successfully, let's connect our database!
|
||||
|
||||
<h2 id="database">Connect a database</h2>
|
||||
|
||||
Our REST API is read-only, so we need to connect our graph API to a database for saving and fetching user data. This tutorial uses SQLite for our SQL database, and Sequelize for our ORM. Our `package.json` already entailed these packages, thus they were installed in the first part of this tutorial with `npm install`. Also, since this section contains some SQL-specific code that isn't necessary to understanding Apollo data sources, we've already built a `UserAPI` data source for you in `src/datasources/user.js`. Please navigate to that file so we can explain the overall concepts.
|
||||
Our REST API is read-only, so we need to connect our graph API to a database for saving and fetching user data. This tutorial uses SQLite for our SQL database, and Sequelize for our ORM. Our `package.json` already included these packages, thus they were installed in the first part of this tutorial with `npm install`. Also, since this section contains some SQL-specific code that isn't necessary to understanding Apollo data sources, we've already built a `UserAPI` data source for you in `src/datasources/user.js`. Please navigate to that file so we can explain the overall concepts.
|
||||
|
||||
<h3 id="custom-data-source">Build a custom data source</h3>
|
||||
|
||||
@@ -137,7 +137,7 @@ Let's go over some of the methods we created in `src/datasources/user.js` to fet
|
||||
- `findOrCreateUser({ email })`: Finds or creates a user with a given `email` in the database
|
||||
- `bookTrips({ launchIds })`: Takes an object with an array of `launchIds` and books them for the logged in user
|
||||
- `cancelTrip({ launchId })`: Takes an object with a `launchId` and cancels that launch for the logged in user
|
||||
- `getLaunchIdsByUser()`: Returns all booked launches for the logged in user.
|
||||
- `getLaunchIdsByUser()`: Returns all booked launches for the logged in user
|
||||
- `isBookedOnLaunch({ launchId })`: Determines whether the logged in user booked a certain launch
|
||||
|
||||
<h2 id="apollo-server">Add data sources to Apollo Server</h2>
|
||||
@@ -160,11 +160,11 @@ const server = new ApolloServer({
|
||||
typeDefs,
|
||||
dataSources: () => ({
|
||||
launchAPI: new LaunchAPI(),
|
||||
userAPI: new UserAPI({ store }),
|
||||
userAPI: new UserAPI({ store })
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
First, we import our `createStore` function to set up our database, as well as our data sources: `LaunchAPI` and `UserAPI`. Then, we create our database by calling `createStore`. Finally, we add the `dataSources` function to our `ApolloServer` to connect `LaunchAPI` and `UserAPI` to our graph. We also pass in our database we created to the `UserAPI` data source.
|
||||
|
||||
Now that we've hooked up our data sources to Apollo Server, it's time to move onto the next section and learn how to call our data sources from within our resolvers.
|
||||
Now that we've hooked up our data sources to Apollo Server, it's time to move on to the next section and learn how to call our data sources from within our resolvers.
|
||||
|
||||
@@ -40,6 +40,7 @@ The tutorial assumes that you're comfortable with JavaScript/ES6, you've fetched
|
||||
<h3 id="system-requirements">System requirements</h3>
|
||||
|
||||
Before we begin, make sure you have:
|
||||
|
||||
- [Node.js](https://nodejs.org/) v6.9.0 or greater
|
||||
- [npm](https://www.npmjs.com/) 3.10.8 or greater
|
||||
- [git](https://git-scm.com/) v2.14.1 or greater
|
||||
@@ -49,6 +50,7 @@ While it's not a requirement, we recommend using [VSCode](https://code.visualstu
|
||||
<h2 id="dev-environment">Set up your development environment</h2>
|
||||
|
||||
Now the fun begins! First, you'll need to install our developer tools:
|
||||
|
||||
- [Apollo Engine (required)](https://engine.apollographql.com) : Our cloud service where you'll register and manage your graph API.
|
||||
- [Apollo DevTools for Chrome (suggested)](https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm) : Our Chrome extension giving you full visibility into your client.
|
||||
- [Apollo VSCode (suggested)](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo): Our editor integration that offers intelligent autocomplete, metrics, and more.
|
||||
@@ -59,7 +61,7 @@ Next, in your terminal, clone [this repository](https://github.com/apollographql
|
||||
git clone https://github.com/apollographql/fullstack-tutorial/
|
||||
```
|
||||
|
||||
There are two folders: one for the starting point (`start/`) and one for the final version (`final`). Within each directory are two folders: one for the server and one for the client. We will be working in the server folder first. If you're comfortable with building a graph API already and you want to skip to the client portion, navigate to the [last half of the tutorial](./client.html).
|
||||
There are two folders: one for the starting point (`start`) and one for the final version (`final`). Within each directory are two folders: one for the server and one for the client. We will be working in the server folder first. If you're comfortable with building a graph API already and you want to skip to the client portion, navigate to the [last half of the tutorial](./client.html).
|
||||
|
||||
<!--
|
||||
TODO: Add in this section after Apollo VSCode works for server development
|
||||
@@ -69,4 +71,4 @@ TODO: Add in this section after Apollo VSCode works for server development
|
||||
|
||||
We know that learning a new technology can sometimes be overwhelming, and it's totally normal to get stuck! If that happens, we recommend joining the [Apollo Spectrum](https://spectrum.chat/apollo) community and posting in the relevant channel (either #apollo-server or #apollo-client) for some quick answers.
|
||||
|
||||
If something in the tutorial seems confusing or contains an error, we'd love your feedback! Just click the Edit on GitHub link at the bottom to open a new pull request or open an issue on the repository.
|
||||
If something in the tutorial seems confusing or contains an error, we'd love your feedback! Just click the Edit on GitHub link on the right side of the page to open a new pull request or open an issue on the repository.
|
||||
|
||||
@@ -7,11 +7,11 @@ Time to accomplish: _15 Minutes_
|
||||
|
||||
Great job for making it this far! We've already learned how to build a GraphQL API with Apollo, connect it to REST and SQL data sources, and send GraphQL queries. Now that we've completed building our graph, it's finally time to deploy it! 🎉
|
||||
|
||||
An Apollo GraphQL API can be deployed to any cloud service, such as Heroku, AWS Lambda, or Netlify. In this tutorial, we'll deploy our graph API to [Zeit Now](https://zeit.co/now). You will need to create a [Now account](https://zeit.co/now) in order to follow these steps. If you haven't already created an [Apollo Engine](https://engine.apollographql.com/) account, you will need to sign up for one.
|
||||
An Apollo GraphQL API can be deployed to any cloud service, such as Heroku, AWS Lambda, or Netlify. In this tutorial, we'll deploy our graph API to [Zeit Now](https://zeit.co/now). You will need to create a [Now account](https://zeit.co/signup) in order to follow these steps. If you haven't already created an [Apollo Engine](https://engine.apollographql.com/) account, you will need to sign up for one.
|
||||
|
||||
<h2 id="engine">Publish your schema to Engine</h2>
|
||||
|
||||
Before we deploy your app, we need to publish our schema to the Apollo Engine cloud service in order to power developer tooling like VSCode and keep track of schema changes. Just like npm is a registry for JavaScript packages, Apollo Engine contains a schema registry that makes it simple to pull the most recent schema from the cloud.
|
||||
Before we deploy our app, we need to publish our schema to the Apollo Engine cloud service in order to power developer tooling like VSCode and keep track of schema changes. Just like npm is a registry for JavaScript packages, Apollo Engine contains a schema registry that makes it simple to pull the most recent schema from the cloud.
|
||||
|
||||
In a production application, you should set up this publishing script as part of your CI workflow. For now, we will run a script in our terminal that uses the Apollo CLI to publish our schema to Engine.
|
||||
|
||||
@@ -58,11 +58,11 @@ Publishing your schema to Apollo Engine unlocks many features necessary for runn
|
||||
- **Schema explorer:** With Engine's powerful schema registry, you can quickly explore all the types and fields in your schema with usage statistics on each field. This metric makes you understand the cost of a field. How expensive is a field? Is a certain field in so much demand?
|
||||
- **Schema history:** Apollo Engine’s schema history allows developers to confidently iterate a graph's schema by validating the new schema against field-level usage data from the previous schema. This empowers developers to avoid breaking changes by providing insights into which clients will be broken by a new schema.
|
||||
- **Performance analytics:** Fine-grained insights into every field, resolvers and operations of your graph's execution
|
||||
- **Performance alerts:** You can configure notifications to be sent to various channels like Slack, and PagerDuty. Apollo Engine can be set to send alerts when a request rate, duration or error rate exceeds a certain threshold.
|
||||
- **Client awareness:** Report client identity (name and version) to your server for insights on client activity.
|
||||
|
||||
We also want to be transparent that the features we just described, such as field metrics, performance alerts, and validating schema changes against recent operations, are only available on a paid plan. Individual developers just getting started with GraphQL probably don't need these features, but they become incredibly valuable as you're working on a team. Additionally, layering these paid features on top of our free developer tools like Apollo VSCode makes them more intelligent over time.
|
||||
We also want to be transparent that the features we just described, such as viewing specific execution traces and validating schema changes against recent operations, are only available on a paid plan. Individual developers just getting started with GraphQL probably don't need these features, but they become incredibly valuable as you're working on a team. Additionally, layering these paid features on top of our free developer tools like Apollo VSCode makes them more intelligent over time.
|
||||
|
||||
We're committed to helping you succeed building and running an Apollo graph API. This is why features such as publishing and downloading schemas from the registry, our open source offerings like Apollo Client and Apollo Server, and certain developer tools like Apollo VSCode and Apollo DevTools will always be free forever.
|
||||
We're committed to helping you succeed in building and running an Apollo graph API. This is why features such as publishing and downloading schemas from the registry, our open source offerings like Apollo Client and Apollo Server, and certain developer tools like Apollo VSCode and Apollo DevTools will always be free forever.
|
||||
|
||||
<h2 id="deploy">Deploy your graph API</h2>
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ Up until now, our graph API hasn't been very useful. We can inspect our graph's
|
||||
Before we can start writing resolvers, we need to learn more about what a resolver function looks like. Resolver functions accept four arguments:
|
||||
|
||||
```js
|
||||
fieldName: (parent, args, context, info) => data
|
||||
fieldName: (parent, args, context, info) => data;
|
||||
```
|
||||
|
||||
* **parent**: An object that contains the result returned from the resolver on the parent type
|
||||
* **args**: An object that contains the arguments passed to the field
|
||||
* **context**: An object shared by all resolvers in a GraphQL operation. We use the context to contain per-request state such as authentication information and access our data sources.
|
||||
* **info**: Information about the execution state of the operation which should only be used in advanced cases
|
||||
- **parent**: An object that contains the result returned from the resolver on the parent type
|
||||
- **args**: An object that contains the arguments passed to the field
|
||||
- **context**: An object shared by all resolvers in a GraphQL operation. We use the context to contain per-request state such as authentication information and access our data sources.
|
||||
- **info**: Information about the execution state of the operation which should only be used in advanced cases
|
||||
|
||||
Remember the `LaunchAPI` and `UserAPI` data sources we created in the previous section and passed to the `context` property of `ApolloServer`? We're going to call them in our resolvers by accessing the `context` argument.
|
||||
|
||||
@@ -38,7 +38,7 @@ const server = new ApolloServer({
|
||||
resolvers,
|
||||
dataSources: () => ({
|
||||
launchAPI: new LaunchAPI(),
|
||||
userAPI: new UserAPI({ store }),
|
||||
userAPI: new UserAPI({ store })
|
||||
})
|
||||
});
|
||||
```
|
||||
@@ -56,17 +56,16 @@ _src/resolvers.js_
|
||||
```js
|
||||
module.exports = {
|
||||
Query: {
|
||||
launches: async (_, __, { dataSources }) =>
|
||||
launches: (_, __, { dataSources }) =>
|
||||
dataSources.launchAPI.getAllLaunches(),
|
||||
launch: (_, { id }, { dataSources }) =>
|
||||
dataSources.launchAPI.getLaunchById({ launchId: id }),
|
||||
me: async (_, __, { dataSources }) =>
|
||||
dataSources.userAPI.findOrCreateUser(),
|
||||
},
|
||||
me: (_, __, { dataSources }) => dataSources.userAPI.findOrCreateUser()
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The code above shows the resolver functions for the `Query` type fields: `launches`, `launch`, and `me`. The first argument to our resolvers, `parent`, is always blank because it refers to the root of our graph. The second argument refers to any `arguments` passed into our query, which we use in our `launch` query to fetch a launch by its id. Finally, we destructure our data sources off the third argument, `context`, in order to call them in our resolvers.
|
||||
The code above shows the resolver functions for the `Query` type fields: `launches`, `launch`, and `me`. The first argument to our _top-level_ resolvers, `parent`, is always blank because it refers to the root of our graph. The second argument refers to any `arguments` passed into our query, which we use in our `launch` query to fetch a launch by its id. Finally, we destructure our data sources from the third argument, `context`, in order to call them in our resolvers.
|
||||
|
||||
Our resolvers are simple and concise because the logic is embedded in the `LaunchAPI` and `UserAPI` data sources. We recommend keeping your resolvers thin as a best practice, which allows you to safely refactor without worrying about breaking your API.
|
||||
|
||||
@@ -119,7 +118,7 @@ query GetLaunchById($id: ID!) {
|
||||
}
|
||||
```
|
||||
|
||||
You can paste `{ "id": 60 }` into the Query Variables section below before running your query. Feel free to experiment with running more queries before moving onto the next section.
|
||||
You can paste `{ "id": 60 }` into the Query Variables section below before running your query. Feel free to experiment with running more queries before moving on to the next section.
|
||||
|
||||
<h3 id="pagination">Paginated queries</h3>
|
||||
|
||||
@@ -181,7 +180,7 @@ module.exports = {
|
||||
const launches = paginateResults({
|
||||
after,
|
||||
pageSize,
|
||||
results: allLaunches,
|
||||
results: allLaunches
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -192,7 +191,7 @@ module.exports = {
|
||||
hasMore: launches.length
|
||||
? launches[launches.length - 1].cursor !==
|
||||
allLaunches[allLaunches.length - 1].cursor
|
||||
: false,
|
||||
: false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -272,9 +271,10 @@ You may be wondering where we're getting the user from in order to fetch their b
|
||||
Access control is a feature that almost every app will have to handle at some point. In this tutorial, we're going to focus on teaching you the essential concepts of authenticating users instead of focusing on a specific implementation.
|
||||
|
||||
Here are the steps you'll want to follow:
|
||||
|
||||
1. The context function on your `ApolloServer` instance is called with the request object each time a GraphQL operation hits your API. Use this request object to read the authorization headers.
|
||||
2. Authenticate the user within the context function.
|
||||
3. Once the user is authenticated, attach the user to the object returned from the context function. This allows us to read the user's information from within our data sources and resolvers, so we can authorize whether they can access the data.
|
||||
1. Authenticate the user within the context function.
|
||||
1. Once the user is authenticated, attach the user to the object returned from the context function. This allows us to read the user's information from within our data sources and resolvers, so we can authorize whether they can access the data.
|
||||
|
||||
Let's open up `src/index.js` and update the `context` function on `ApolloServer` to the code shown below:
|
||||
|
||||
@@ -302,7 +302,7 @@ const server = new ApolloServer({
|
||||
|
||||
Just like in the steps outlined above, we're checking the authorization headers on the request, authenticating the user by looking up their credentials in the database, and attaching the user to the `context`. While we definitely don't advocate using this specific implementation in production since it's not secure, all of the concepts outlined here are transferable to how you'll implement authentication in a real world application.
|
||||
|
||||
How do we create the token passed to the `authorization` headers? Let's move onto the next section, so we can write our resolver for the `login` mutation.
|
||||
How do we create the token passed to the `authorization` headers? Let's move on to the next section, so we can write our resolver for the `login` mutation.
|
||||
|
||||
<h2 id="mutation">Write Mutation resolvers</h2>
|
||||
|
||||
@@ -345,7 +345,7 @@ Mutation: {
|
||||
};
|
||||
},
|
||||
cancelTrip: async (_, { launchId }, { dataSources }) => {
|
||||
const result = dataSources.userAPI.cancelTrip({ launchId });
|
||||
const result = await dataSources.userAPI.cancelTrip({ launchId });
|
||||
|
||||
if (!result)
|
||||
return {
|
||||
@@ -383,7 +383,7 @@ Now, let's try booking some trips. Only authorized users are permitted to book t
|
||||
|
||||
```graphql
|
||||
mutation BookTrips {
|
||||
bookTrips(launchIds: [67,68,69]) {
|
||||
bookTrips(launchIds: [67, 68, 69]) {
|
||||
success
|
||||
message
|
||||
launches {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "1. Build a schema"
|
||||
title: '1. Build a schema'
|
||||
description: Create a blueprint for your graph's data
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ The first step on our journey toward building our graph API is constructing its
|
||||
|
||||
Before we write our schema, we need to set up our graph API's server. **Apollo Server** is a library that helps you build a production-ready graph API over your data. It can connect to any data source, including REST APIs and databases, and it seamlessly integrates with Apollo developer tooling.
|
||||
|
||||
From the root, let's install our projects dependencies:
|
||||
From the root, let's install our project's dependencies:
|
||||
|
||||
```bash
|
||||
cd start/server && npm install
|
||||
@@ -38,20 +38,20 @@ Schemas are at their best when they are designed around the needs of the clients
|
||||
|
||||
Let's think about the data we will need to expose in order to build our app. Our app needs to:
|
||||
|
||||
* Fetch all upcoming rocket launches
|
||||
* Fetch a specific launch by its ID
|
||||
* Login the user
|
||||
* Book launch trips if the user is logged in
|
||||
* Cancel launch trips if the user is logged in
|
||||
- Fetch all upcoming rocket launches
|
||||
- Fetch a specific launch by its ID
|
||||
- Login the user
|
||||
- Book launch trips if the user is logged in
|
||||
- Cancel launch trips if the user is logged in
|
||||
|
||||
Our schema will be based on these features. In `src/schema.js`, import `gql` from Apollo Server and create a variable called `typeDefs` for your schema. Your schema will go inside the `gql` function (between the backticks in this portion: <code>gql``</code>).
|
||||
Our schema will be based on these features. In `src/schema.js`, import `gql` from Apollo Server and create a variable called `typeDefs` for your schema. Your schema will go inside the `gql` function (between the backticks in this portion: <code>gql\`\`</code>).
|
||||
|
||||
_src/schema.js_
|
||||
|
||||
```js
|
||||
const { gql } = require('apollo-server');
|
||||
|
||||
const typeDefs = gql``
|
||||
const typeDefs = gql``;
|
||||
|
||||
module.exports = typeDefs;
|
||||
```
|
||||
@@ -157,7 +157,7 @@ type TripUpdateResponse {
|
||||
}
|
||||
```
|
||||
|
||||
Our mutation response type contains a success status, a corresponding message, and the launch that we updated. It's always good practice to return the data that you're updating in order for the Apollo Client cache to update automatically.
|
||||
Our mutation response type contains a success status, a corresponding message, and the launch that we updated. It's always good practice to return the data that you're updating in order for the Apollo Client cache to update automatically.
|
||||
|
||||
<h2 id="apollo-server-run">Run your server</h2>
|
||||
|
||||
@@ -184,7 +184,6 @@ By default, Apollo Server supports [GraphQL Playground](/docs/apollo-server/feat
|
||||
|
||||
The GraphQL Playground provides the ability to introspect your schema. **Introspection** is a technique used to provide detailed information about a graph's schema. To see this in action, check out the right hand side of GraphQL Playground and click on the `schema` button.
|
||||
|
||||
|
||||
<div style="text-align:center">
|
||||
<img src="../images/schematab.png" alt="Schema button">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user