Add credential table (#3710)
This commit is contained in:
parent
25c66e6901
commit
2ae520fb8a
7 changed files with 157 additions and 102 deletions
|
@ -46,7 +46,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keycloak/keycloak-admin-client": "^19.0.3",
|
"@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
|
||||||
"@patternfly/patternfly": "^4.219.2",
|
"@patternfly/patternfly": "^4.219.2",
|
||||||
"@patternfly/react-code-editor": "^4.82.55",
|
"@patternfly/react-code-editor": "^4.82.55",
|
||||||
"@patternfly/react-core": "^4.258.3",
|
"@patternfly/react-core": "^4.258.3",
|
||||||
|
|
|
@ -150,6 +150,7 @@
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"userLabel": "User label",
|
"userLabel": "User label",
|
||||||
"data": "Data",
|
"data": "Data",
|
||||||
|
"providedBy": "Provided by",
|
||||||
"passwordDataTitle": "Password data",
|
"passwordDataTitle": "Password data",
|
||||||
"updateCredentialUserLabelSuccess": "The user label has been changed successfully.",
|
"updateCredentialUserLabelSuccess": "The user label has been changed successfully.",
|
||||||
"updateCredentialUserLabelError": "Error changing user label: {{error}}",
|
"updateCredentialUserLabelError": "Error changing user label: {{error}}",
|
||||||
|
|
|
@ -94,6 +94,10 @@ export const ResourcesPolicySelect = ({
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.flat()
|
.flat()
|
||||||
|
.filter(
|
||||||
|
(r): r is PolicyRepresentation | ResourceRepresentation =>
|
||||||
|
typeof r !== "string"
|
||||||
|
)
|
||||||
.map(convert)
|
.map(convert)
|
||||||
.filter(
|
.filter(
|
||||||
({ id }, index, self) =>
|
({ id }, index, self) =>
|
||||||
|
|
|
@ -134,24 +134,13 @@ export default function NewAttributeSettings() {
|
||||||
flatten<any, any>({ permissions, selector, required }, { safe: true })
|
flatten<any, any>({ permissions, selector, required }, { safe: true })
|
||||||
).map(([key, value]) => form.setValue(key, value));
|
).map(([key, value]) => form.setValue(key, value));
|
||||||
form.setValue("annotations", convert(annotations));
|
form.setValue("annotations", convert(annotations));
|
||||||
form.setValue("validations", convert(validations));
|
form.setValue("validations", validations);
|
||||||
form.setValue("isRequired", required !== undefined);
|
form.setValue("isRequired", required !== undefined);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const save = async (profileConfig: UserProfileAttributeType) => {
|
const save = async (profileConfig: UserProfileAttributeType) => {
|
||||||
const validations = profileConfig.validations?.reduce(
|
|
||||||
(prevValidations: any, currentValidations: any) => {
|
|
||||||
prevValidations[currentValidations.key] =
|
|
||||||
currentValidations.value?.length === 0
|
|
||||||
? {}
|
|
||||||
: currentValidations.value;
|
|
||||||
return prevValidations;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const annotations = (profileConfig.annotations! as KeyValueType[]).reduce(
|
const annotations = (profileConfig.annotations! as KeyValueType[]).reduce(
|
||||||
(obj, item) => Object.assign(obj, { [item.key]: item.value }),
|
(obj, item) => Object.assign(obj, { [item.key]: item.value }),
|
||||||
{}
|
{}
|
||||||
|
@ -169,7 +158,6 @@ export default function NewAttributeSettings() {
|
||||||
...attribute,
|
...attribute,
|
||||||
name: attributeName,
|
name: attributeName,
|
||||||
displayName: profileConfig.displayName!,
|
displayName: profileConfig.displayName!,
|
||||||
validations,
|
|
||||||
selector: profileConfig.selector,
|
selector: profileConfig.selector,
|
||||||
permissions: profileConfig.permissions!,
|
permissions: profileConfig.permissions!,
|
||||||
annotations,
|
annotations,
|
||||||
|
@ -188,7 +176,6 @@ export default function NewAttributeSettings() {
|
||||||
name: profileConfig.name,
|
name: profileConfig.name,
|
||||||
displayName: profileConfig.displayName!,
|
displayName: profileConfig.displayName!,
|
||||||
required: profileConfig.isRequired ? profileConfig.required : {},
|
required: profileConfig.isRequired ? profileConfig.required : {},
|
||||||
validations,
|
|
||||||
selector: profileConfig.selector,
|
selector: profileConfig.selector,
|
||||||
permissions: profileConfig.permissions!,
|
permissions: profileConfig.permissions!,
|
||||||
annotations,
|
annotations,
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { CredentialRow } from "./user-credentials/CredentialRow";
|
||||||
import { toUpperCase } from "../util";
|
import { toUpperCase } from "../util";
|
||||||
|
|
||||||
import "./user-credentials.css";
|
import "./user-credentials.css";
|
||||||
|
import { FederatedCredentials } from "./user-credentials/FederatedCredentials";
|
||||||
|
|
||||||
type UserCredentialsProps = {
|
type UserCredentialsProps = {
|
||||||
user: UserRepresentation;
|
user: UserRepresentation;
|
||||||
|
@ -361,7 +362,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{groupedUserCredentials.length !== 0 ? (
|
{groupedUserCredentials.length !== 0 && (
|
||||||
<>
|
<>
|
||||||
{user.email && (
|
{user.email && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -374,10 +375,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PageSection variant={PageSectionVariants.light}>
|
<PageSection variant={PageSectionVariants.light}>
|
||||||
<TableComposable
|
<TableComposable variant={"compact"}>
|
||||||
aria-label="userCredentials-table"
|
|
||||||
variant={"compact"}
|
|
||||||
>
|
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr className="kc-table-header">
|
<Tr className="kc-table-header">
|
||||||
<Th>
|
<Th>
|
||||||
|
@ -490,26 +488,31 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
||||||
</TableComposable>
|
</TableComposable>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<ListEmptyState
|
|
||||||
hasIcon={true}
|
|
||||||
message={t("noCredentials")}
|
|
||||||
instructions={t("noCredentialsText")}
|
|
||||||
primaryActionText={t("setPassword")}
|
|
||||||
onPrimaryAction={toggleModal}
|
|
||||||
secondaryActions={
|
|
||||||
user.email
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
text: t("credentialResetBtn"),
|
|
||||||
onClick: toggleCredentialsResetModal,
|
|
||||||
type: ButtonVariant.link,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
{(user.federationLink || user.origin) && (
|
||||||
|
<FederatedCredentials user={user} onSetPassword={toggleModal} />
|
||||||
|
)}
|
||||||
|
{groupedUserCredentials.length === 0 &&
|
||||||
|
!(user.federationLink || user.origin) && (
|
||||||
|
<ListEmptyState
|
||||||
|
hasIcon
|
||||||
|
message={t("noCredentials")}
|
||||||
|
instructions={t("noCredentialsText")}
|
||||||
|
primaryActionText={t("setPassword")}
|
||||||
|
onPrimaryAction={toggleModal}
|
||||||
|
secondaryActions={
|
||||||
|
user.email
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
text: t("credentialResetBtn"),
|
||||||
|
onClick: toggleCredentialsResetModal,
|
||||||
|
type: ButtonVariant.link,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
112
apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx
Normal file
112
apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
PageSection,
|
||||||
|
PageSectionVariants,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
|
||||||
|
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||||
|
import { useAccess } from "../../context/access/Access";
|
||||||
|
import { toUserFederationLdap } from "../../user-federation/routes/UserFederationLdap";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
type FederatedCredentialsProps = {
|
||||||
|
user: UserRepresentation;
|
||||||
|
onSetPassword: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FederatedCredentials = ({
|
||||||
|
user,
|
||||||
|
onSetPassword,
|
||||||
|
}: FederatedCredentialsProps) => {
|
||||||
|
const { t } = useTranslation("users");
|
||||||
|
const { adminClient } = useAdminClient();
|
||||||
|
const { realm } = useRealm();
|
||||||
|
|
||||||
|
const [credentialTypes, setCredentialTypes] = useState<string[]>();
|
||||||
|
const [component, setComponent] = useState<ComponentRepresentation>();
|
||||||
|
const access = useAccess();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() =>
|
||||||
|
Promise.all([
|
||||||
|
adminClient.users.getUserStorageCredentialTypes({ id: user.id! }),
|
||||||
|
access.hasAccess("view-realm")
|
||||||
|
? adminClient.components.findOne({
|
||||||
|
id: (user.federationLink || user.origin)!,
|
||||||
|
})
|
||||||
|
: adminClient.userStorageProvider.name({
|
||||||
|
id: (user.federationLink || user.origin)!,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
([credentialTypes, component]) => {
|
||||||
|
setCredentialTypes(credentialTypes);
|
||||||
|
setComponent(component);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!credentialTypes || !component) {
|
||||||
|
return <KeycloakSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection variant={PageSectionVariants.light}>
|
||||||
|
<TableComposable variant={"compact"}>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t("type")}</Th>
|
||||||
|
<Th>{t("providedBy")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{credentialTypes.map((credential) => (
|
||||||
|
<Tr key={credential}>
|
||||||
|
<Td>
|
||||||
|
<b>{credential}</b>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
isDisabled={!access.hasAccess("view-realm")}
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={toUserFederationLdap({
|
||||||
|
id: component.id!,
|
||||||
|
realm,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{component.name}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
{credential === "password" && (
|
||||||
|
<Td modifier="fitContent">
|
||||||
|
<Button variant="secondary" onClick={onSetPassword}>
|
||||||
|
{t("setPassword")}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
74
package-lock.json
generated
74
package-lock.json
generated
|
@ -140,7 +140,7 @@
|
||||||
},
|
},
|
||||||
"apps/admin-ui": {
|
"apps/admin-ui": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keycloak/keycloak-admin-client": "^19.0.3",
|
"@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
|
||||||
"@patternfly/patternfly": "^4.219.2",
|
"@patternfly/patternfly": "^4.219.2",
|
||||||
"@patternfly/react-code-editor": "^4.82.55",
|
"@patternfly/react-code-editor": "^4.82.55",
|
||||||
"@patternfly/react-core": "^4.258.3",
|
"@patternfly/react-core": "^4.258.3",
|
||||||
|
@ -2953,13 +2953,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@keycloak/keycloak-admin-client": {
|
"node_modules/@keycloak/keycloak-admin-client": {
|
||||||
"version": "19.0.3",
|
"version": "21.0.0-dev.3",
|
||||||
"license": "Apache-2.0",
|
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz",
|
||||||
|
"integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"camelize-ts": "^2.1.1",
|
"camelize-ts": "^2.1.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"query-string": "^7.0.1",
|
|
||||||
"url-join": "^5.0.0",
|
"url-join": "^5.0.0",
|
||||||
"url-template": "^3.0.0"
|
"url-template": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
@ -6822,6 +6822,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/decode-uri-component": {
|
"node_modules/decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
|
@ -8354,13 +8355,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filter-obj": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/find-cache-dir": {
|
"node_modules/find-cache-dir": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -11726,22 +11720,6 @@
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/query-string": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"decode-uri-component": "^0.2.0",
|
|
||||||
"filter-obj": "^1.1.0",
|
|
||||||
"split-on-first": "^1.0.0",
|
|
||||||
"strict-uri-encode": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/querystring": {
|
"node_modules/querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -12856,13 +12834,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/split-on-first": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/split-string": {
|
"node_modules/split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -13054,13 +13025,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/strict-uri-encode": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -16898,12 +16862,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@keycloak/keycloak-admin-client": {
|
"@keycloak/keycloak-admin-client": {
|
||||||
"version": "19.0.3",
|
"version": "21.0.0-dev.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz",
|
||||||
|
"integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"camelize-ts": "^2.1.1",
|
"camelize-ts": "^2.1.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"query-string": "^7.0.1",
|
|
||||||
"url-join": "^5.0.0",
|
"url-join": "^5.0.0",
|
||||||
"url-template": "^3.0.0"
|
"url-template": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
@ -18311,7 +18276,7 @@
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.19.4",
|
||||||
"@cypress/webpack-batteries-included-preprocessor": "^2.2.3",
|
"@cypress/webpack-batteries-included-preprocessor": "^2.2.3",
|
||||||
"@cypress/webpack-preprocessor": "^5.15.3",
|
"@cypress/webpack-preprocessor": "^5.15.3",
|
||||||
"@keycloak/keycloak-admin-client": "^19.0.3",
|
"@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"@patternfly/patternfly": "^4.219.2",
|
"@patternfly/patternfly": "^4.219.2",
|
||||||
"@patternfly/react-code-editor": "^4.82.55",
|
"@patternfly/react-code-editor": "^4.82.55",
|
||||||
|
@ -19725,7 +19690,8 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0"
|
"version": "0.2.0",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"decompress": {
|
"decompress": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
|
@ -20742,9 +20708,6 @@
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter-obj": {
|
|
||||||
"version": "1.1.0"
|
|
||||||
},
|
|
||||||
"find-cache-dir": {
|
"find-cache-dir": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -22943,15 +22906,6 @@
|
||||||
"version": "6.5.3",
|
"version": "6.5.3",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"query-string": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"requires": {
|
|
||||||
"decode-uri-component": "^0.2.0",
|
|
||||||
"filter-obj": "^1.1.0",
|
|
||||||
"split-on-first": "^1.0.0",
|
|
||||||
"strict-uri-encode": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
@ -23686,9 +23640,6 @@
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"split-on-first": {
|
|
||||||
"version": "1.1.0"
|
|
||||||
},
|
|
||||||
"split-string": {
|
"split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -23830,9 +23781,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strict-uri-encode": {
|
|
||||||
"version": "2.0.0"
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
Loading…
Reference in a new issue